Each file must include the Apache license information as a header.
/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */
We recommend to follow the {{< docs_link file=“flink-docs-stable/docs/flinkdev/ide_setup/” name=“IDE Setup Guide”>}} to get IDE tooling configured.
Golden rule: Comment as much as necessary to support code understanding, but don’t add redundant information.
Think about
The code alone should explain as much as possible the “what” and the “how”
In-code comments help explain the “why”
// this specific code layout helps the JIT to better do this or that// nulling out this field here means future write attempts are fail-fast// for arguments with which this method is actually called, this seemingly naive approach works actually better than any optimized/smart versionIn-code comments should not state redundant information about the “what” and “how” that is already obvious in the code itself.
JavaDocs should not state meaningless information (just to satisfy the Checkstyle checker).
Don’t:
/**
* The symbol expression.
*/
public class CommonSymbolExpression {}
Do:
/**
* An expression that wraps a single specific symbol.
* A symbol could be a unit, an alias, a variable, etc.
*/
public class CommonSymbolExpression {}
Avoid deep nesting of scopes, by flipping the if condition and exiting early.
Don’t:
if (a) {
if (b) {
if (c) {
the main path
}
}
}
Do
if (!a) {
return ..
}
if (!b) {
return ...
}
if (!c) {
return ...
}
the main path
While it is hard to exactly specify what constitutes a good design, there are some properties that can serve as a litmus test for a good design. If these properties are given, the chances are good that the design is going into a good direction. If these properties cannot be achieved, there is a high probability that the design is flawed.
final as possible.final fields (except maybe auxiliary fields, like lazy cached hash codes).init() or setup() methods. Once the constructor completes, the object should be usable.For nullability, the Flink codebase aims to follow these conventions:
@javax.annotation.Nullable. That way, you get warnings from IntelliJ about all sections where you have to reason about potential null values.Note: This means that @Nonnull annotations are usually not necessary, but can be used in certain cases to override a previous annotation, or to point non-nullability out in a context where one would expect a nullable value.
Optional is a good solution as a return type for method that may or may not have a result, so nullable return types are good candidates to be replaced with Optional. See also [usage of Java Optional]({{< relref “how-to-contribute/code-style-and-quality-java” >}}#java-optional).
Code that is easily testable typically has good separation of concerns and is structured to be reusable outside the original context (by being easily reusable in tests).
A good summary or problems / symptoms and recommended refactoring is in the PDF linked below. Please note that while the examples in the PDF often use a dependency injection framework (Guice), it works in the same way without such a framework.[^1]
http://misko.hevery.com/attachments/Guide-Writing%20Testable%20Code.pdf
Here is a compact summary of the most important aspects.
Inject dependencies
Reusability becomes easier if constructors don’t create their dependencies (the objects assigned to the fields), but accept them as parameters.
new keyword.new ArrayList<>()) or similar auxiliary fields (objects that have only primitive dependencies).To make instantiation easy / readable, add factory methods or additional convenience constructors to construct whole object with dependencies.
In no case should it ever be required to use a reflection or a “Whitebox” util to change the fields of an object in a test, or to use PowerMock to intercept a “new” call and supply a mock.
Avoid “too many collaborators”
If you have to take a big set of other components into account during testing (“too many collaborators”), consider refactoring.
The component/class you want to test probably depends on another broad component (and its implementation), rather than on the minimal interface (abstraction) required for its work.
In that case, segregate the interfaces (factor out the minimal required interface) and supply a test stub in that case.
⇒ Please note that these steps often require more effort in implementing tests (factoring out interfaces, creating dedicated test stubs), but make the tests more resilient to changes in other components, i.e., you do not need to touch the tests when making unrelated changes.
We can conceptually distinguish between code that “coordinates” and code that “processes data”. Code that coordinates should always favor simplicity and cleanness. Data processing code is highly performance critical and should optimize for performance.
That means still applying the general idea of the sections above, but possibly forgoing some aspects in some place, in order to achieve higher performance.
Which code paths are Data Processing paths?
Things that performance critical code may do that we would otherwise avoid
Most code paths should not require any concurrency. The right internal abstractions should obviate the need for concurrency in almost all cases.
When developing a component think about threading model and synchronization points ahead.
Try to avoid using threads all together if possible in any way.
Be aware that using threads is in fact much harder than it initially looks
Be aware of the java.util.concurrent.CompletableFuture
CompletableFuture.supplyAsync(value, executor) in that case, instead of future.complete(value) when an executor is availableCompletableFuture.allOf()/anyOf(), ExecutorCompletionService, or org.apache.flink.runtime.concurrent.FutureUtils#waitForAll if you need to wait for: all the results/any of the results/all the results but handled by (approximate) completion order.common module in this case.We are moving our codebase to JUnit 5 and AssertJ as our testing framework and assertions library of choice.
Unless there is a specific reason, make sure you use JUnit 5 and AssertJ when contributing to Flink with new tests and even when modifying existing tests. Don‘t use Hamcrest, JUnit assertions and assert directive. Make your tests readable and don’t duplicate assertions logic provided by AssertJ or by custom assertions provided by some flink modules. For example, avoid:
assert list.size() == 10; for (String item : list) { assertTrue(item.length() < 10); }
And instead use:
assertThat(list) .hasSize(10) .allMatch(item -> item.length() < 10);
Test contracts not implementations: Test that after a sequence of actions, the components are in a certain state, rather than testing that the components followed a sequence of internal state modifications.
A way to enforce this is to try to follow the Arrange, Act, Assert test structure when writing a unit test (https://xp123.com/articles/3a-arrange-act-assert/)
This helps to communicate the intention of the test (what is the scenario under test) rather than the mechanics of the tests. The technical bits go to a static methods at the bottom of the test class.
Example of tests in Flink that follow this pattern are:
Generally speaking, we should avoid setting local timeouts in JUnit tests but rather depend on the global timeout in Azure. The global timeout benefits from taking thread dumps just before timing out the build, easing debugging.
At the same time, any timeout value that you manually set is arbitrary. If it‘s set too low, you get test instabilities. What too low means depends on numerous factors, such as hardware and current utilization (especially I/O). Moreover, a local timeout is more maintenance-intensive. It’s one more knob where you can tweak a build. If you change the test a bit, you also need to double-check the timeout. Hence, there have been quite a few commits that just increase timeouts.
[^1]: We are keeping such frameworks out of Flink, to make debugging easier and avoid dependency clashes.