-
Notifications
You must be signed in to change notification settings - Fork 20
Home
Trickle is syntactic sugar on top of ListenableFutures. Its objective is to make it easier to understand code that implements non-trivial asynchronous call graphs. A typical example of when it is useful in a Spotify context is a search view, where you need to make the following calls:
- Find out which tracks match a certain search query.
- Based on the search results, fetch data such as album covers, composers, artists, etc., about each track.
- Based on the search results, find out from a separate service how many playlists each track is included in.
- When all the results are ready, convert them into the format the caller expects and return.
Doing this synchronously is easy, but asynchronously using raw ListenableFutures it gets a lot harder. Trickle makes it more pleasant.
- Improve readability of code making nested asynchronous calls, in particular with regard to making the relationships between different calls clearer.
- Type-safety and IDE-friendliness.
- Great error handling - ensure errors are always reported to the caller, unless explicitly configured to be hidden.
- Making the fact that the business logic is executed in a concurrent context as hidden as possible. This means that a) application code shouldn't be able to interrupt the concurrent flow (so inputs should never be futures - application code should only be invoked when results are ready), and b) it's important to move as many as possible of the hard concurrency problems into the framework rather than solve them in application code.
The following diagram shows a Trickle Graph, where the colour of a node indicates which function is executed in it.
- Graph: Trickle allows you to connect a set of calls into a graph. Graphs in Trickle have the following characteristics:
- They are directed and acyclic.
- They can have any number of named input parameters.
- They have a single sink that returns the result of invoking the Graph. A sink in Trickle is a node whose output no other nodes need as inputs, and which no other nodes need to be executed after.
- Edges are either parameter dependencies (the function in node B uses the result of node A), or time dependencies (node B doesn't need the result of node A, but needs to happen afterwards).
- Node: there are three kinds of nodes in Trickle - graph parameters, which are values that can vary with each graph execution, inner nodes and the single sink node.
- Inner nodes and the sink contain Functions that are executed with the inputs defined when connecting the node to the Graph. It is possible to execute the same exact Function in different Nodes in the same Graph.
- Node inputs are defined using the
with()
method. An input is either a Name for a graph parameter or a previously defined Graph. - Predecessors are defined using the
after()
method. A predecessor is a graph that needs to be completed before the Function in this node can be invoked. The difference between a predecessor and an input is that we don't care about the value of the predecessor, so it's not going to be passed as a parameter to the function. - A fallback can be defined using the
fallback()
method. This fallback will get invoked in case of an exception and returns a fallback value to use instead. - Nodes can be assigned a name using the
named()
method. This is just for debugging your graph. - The sink node is special in that it defines the output of the Graph, and is also the node that downstream nodes get connected to when a dependency is established.
- Function: A function takes some parameters and returns a future to a result. Note that Trickle Functions are not pure functions, and thus not required to be side-effect-free. When a function is wired into a Graph, a Node is created.
- Graph Parameter/Name: call graphs are often quite static, just like the code in a regular method. But there are usually some inputs to the graph that need to vary from invocation to invocation. These are parameters that you can Name and have to bind to concrete values before invoking a graph.
At each step when building a Trickle Graph, you've got a complete graph, and you build up the final Graph incrementally, by adding new sink nodes. To construct the Graph in the diagram in the Concepts section, you could do this, ignoring all types:
Graph<T> g1 = Trickle.call(function1).with(INPUT_A);
// g1 is a complete graph that can be run on its own
// connect node2 to the sink node of g1
Graph<T> g2 = Trickle.call(function2).with(INPUT_A, g1);
// now g2 consists of three nodes: input A, node 1 and node 2.
Graph<T> g3 = Trickle.call(function2).with(g1, INPUT_B);
// now g3 consists of four nodes: input A, node 1, input B and node 3.
Graph<T> g4 = Trickle.call(function3).with(g2).after(g3);
// g4 is the complete graph with all the nodes in the diagram.
Note that the with()
method takes either a name of an input parameter or a previously defined sub-graph. The latter case means that the new node depends on the output of the sink node of the previous sub-graph.
For examples, see Examples.java.
Other options that can help you simplify writing asynchronous code in Java that we know of and like are:
- RxJava - https://github.com/Netflix/RxJava
- Akka - http://akka.io/
For some reasoning around design choices we made, see Design Rationales.
For an as-yet incomplete list of best practices, see Best Practices.