2. Synchronous
● Result is ready when method returns
● What about methods void e.g.
void deletePerson(Long id)
● Unchecked exception (or it’s lack) is part of a
result
● Error handling through optional try-catch
mechanism
Person fetchPerson(Long id)
2
3. Asynchronous
● Result may be ready long after method
returns
● There must be a way to access result e.g.
using Callback or Promise
● Custom error handling
void fetchPerson(Long id, Callback callback)
void Promise<Person> fetchPerson(Long id)
3
7. Challenges
● Indeterminism
○ n! many orders in which all asynchronous operations
can finish
○ Difficult to test
○ No “current state”
● Debugging
○ State of the art is println() sprinkled around
○ No meaningful stack trace
7
8. Challenges
● Thread safety
○ Synchronized, locks, volatile, atomic, j.u.c.*
○ Immutability
○ 3rd party libraries
○ Thinking about Java Memory Model
○ Thread safety does not mean atomicity
8
9. Challenges
● Often requires all-async
//fetching Alice and Bob asynchronously
fetchPerson(aliceId, handleAliceCallback);
fetchPerson(bobId, handleBobCallback);
// hmmm... now what?
9
10. Core concepts: Promise<T>
● Represents state of an asynchronous
operation
● Possible states: completed (resolved),
uncompleted (unresolved)
● Container for a result of an asynchronous
operation
● Result of completed Promise<T> is either
value of type T or an exception 10
11. Core concepts: Task<T>
● Main abstraction and building block in
ParSeq, represents asynchronous operation
● Main ingredient of a Task<T> is a clojure
that (synchronously) returns a Promise<T>,
as if Task<T> had void Promise<T> run(...)
● Clojure starts asynchronous operation and
returns a Promise that represents the state
of that operation 11
14. Core concepts: Task<T>
public <T> Promise<Response<T>> sendRequest(Request<T> request,
RequestContext requestContext) {
SettablePromise<Response<T>> promise = Promises.settable();
_restClient.sendRequest(request, requestContext,
new PromiseCallbackAdapter<T>(promise));
return promise;
}
Task<Response<T>> task =
Task.async(name, () -> sendRequest(request, requestContext));
● Real example from ParSeqRestClient
14
15. Core concepts: Task<T>
public <T> Promise<Response<T>> sendRequest(Request<T> request,
RequestContext requestContext) {
SettablePromise<Response<T>> promise = Promises.settable();
_restClient.sendRequest(request, requestContext,
new PromiseCallbackAdapter<T>(promise));
return promise;
}
Task<Response<T>> task =
Task.async(name, () -> sendRequest(request, requestContext));
● Real example from ParSeqRestClient
clojure
15
16. Core concepts: Task<T>
time
● Example execution of tasks
○ clojure synchronously returns a Promise<T>
○ Promise is completed at some point in the future
clojure
execution
Promise
completion
16
17. Properties of Tasks
● Tasks are lazy - creating a Task instance
does not automatically run it
● Task runs at most once
● Tasks are immutable - all Task.* methods
return new Tasks
● Programming with Tasks comes down to
creating new Tasks that depend on existing
ones (composition) 17
18. Creating Tasks from scratch
● Usually there is no need to create Task from
scratch using Task.async(...), instead use
existing library e.g.
_parseqRestClient.createTask(...)
● If there is a need to create a Task from
scratch use Task.async(...), don’t extend
BaseTask class
18
22. Sequential Composition
● Do something after Task (it’s Promise) is completed
Task<String> seq = fetchBing.andThen("seq", fetchGoogle);
22
23. Sequential Composition
● Do something after Task (it’s Promise) is completed
Task<String> seq = fetchBing.andThen("seq", fetchGoogle);
time
23
24. Sequential Composition
● Do something after Task (it’s Promise) is completed
Task<String> seq = fetchBing.andThen("seq", fetchGoogle);
time
clojure
execution
Promise
completion
24
25. ParSeq API: map
● Transforms result of a task
Task<String> name = Task.value("Jaroslaw");
Task<Integer> length = name.map(String::length);
25
26. ParSeq API: flatMap
● Runs Task that depends on result of previous Task
● fetchCompany() invoked after fetchPerson() completes
public Task<Company> fetchCompany(int id) {...}
public Task<Person> fetchPerson(int id) {...}
Task<Person> personTask = fetchPerson(id);
Task<Company> companyTask =
personTask.flatMap(person ->
fetchCompany(person.getCompanyId()));
vs
Task<Task<Company>> companyTask =
personTask.map(person ->
fetchCompany(person.getCompanyId()));
26
27. Exceptions handling
● All exceptions are automatically propagated
Task<Integer> fetchAndLength =
fetch404Url("http://www.google.com/idontexist")
.map("length", String::length);
● Clojures can throw checked and unchecked exceptions
fetchBody("http://www.bing.com")
.andThen(s -> { throw new IOException(); });
27
28. Exceptions handling
● Task API has methods to handle exceptions, equivalent
to try-catch for synchronous methods, e.g. recover()
Task<Integer> fetchAndLength =
fetch404Url("http://www.google.com/idontexist")
.recover("default", t -> "")
.map("length", s -> s.length());
28
29. Exceptions handling
● Exceptions can be handled explicitly, see methods
toTry(), transform()
● Try<T> interface has only two implementation:
Success<T> and Failure<T>
Task<Try<String>> tryFetch =
fetch404Url("http://www.google.com/idontexist")
.toTry();
29
30. withTimeout
● Fails Task if it is not completed within specified amount
of time
Task<String> fetchWithTimeout =
fetchUrl("http://www.google.com")
.withTimeout(15, TimeUnit.MILLISECONDS);
30
31. withTimeout
● Fails Task if it is not completed within specified amount
of time
Task<String> fetchWithTimeout =
fetchUrl("http://www.google.com")
.withTimeout(15, TimeUnit.MILLISECONDS);
time
31
32. ParSeq API
● ParSeq API is declarative
● Focus on “what” not “how”
● Task<T> interface does not have run() method
32
33. ParSeq programming model
● Express everything by composing and
transforming Tasks
● End up with one Task (Plan) that is passed
to ParSeq Engine for execution
● Rest.li implements this idea
@RestMethod.Get
public Task<Greeting> get(final Long id) {
// rest.li resource implementation
} 33
34. ParSeq execution model
● ParSeq Engine provides the following
guarantees:
○ Rules expressed in ParSeq API are followed e.g.
first.andThen(second) - “second” will run after “first”
is completed
○ All clojures are executed sequentially, such that
clojure of a previous Task in a sequence “happens
before” clojure of a next Task (“happens before” in
the Java Memory Model sense)
34
35. Clojures are executed sequentially
● All operations passed to Task.* API are
executed within a Task’s clojure
Task<Integer> length =
fetchBody("http://www.google.com")
.map("length", String::length);
time
35
36. Clojures are executed sequentially
● All operations passed to Task.* API are
executed within a clojure
Task<Integer> length =
fetchBody("http://www.google.com")
.map("length", String::length);
time
clojure
36
37. Clojures are executed sequentially
● All operations passed to Task.* API are
automatically thread safe and atomic
● No need to be concerned about:
○ Synchronized, locks, volatile, atomic, j.u.c.*
○ Immutability
○ 3rd party libraries
○ Thinking about Java Memory Model
○ Thread safety does not mean atomicity
37
38. Clojures are executed sequentially
● Example: find top N Candidates (synchronous)
PriorityQueue<Candidate> topCandidates = new PriorityQueue<>();
void considerCandidate(Candidate candidate) {
topCandidates.add(candidate);
if (topCandidates.size() > N) {
topCandidates.remove();
}
}
38
39. Clojures are executed sequentially
● Example: find top N Candidates (asynchronous)
Task<Candidate> fetchCandidate(Long Id) {...}
Task<?> considerCandidate(Long id) {
return fetchCandidate(id)
.andThen(this::considerCandidate);
}
● Using thread safe PriorityQueue is not enough
● No need to test concurrency aspect 39
40. Clojures are executed sequentially
Task<?> par3 = Task.par(fetchBing, fetchGoogle, fetchYahoo)
time
● Task.par(t1, t2, …) example
40
42. Clojures are executed sequentially
Task.par(
Task.callable("1000th prime", () -> nthPrime(1000)),
Task.callable("1000th prime", () -> nthPrime(1000)),
Task.callable("1000th prime", () -> nthPrime(1000))
);
● Task.par(t1, t2, …) example
time
42
43. Clojures are executed sequentially
● Does it mean that ParSeq is single threaded?
● No
Task.par(
fetchBody("http://www.google.com").andThen(this::processBody),
fetchBody("http://www.bing.com").andThen(this::processBody)
);
43
44. Clojures are executed sequentially
● Does it mean that ParSeq is single threaded?
● No
time
other tasks
other tasks
wait
processBodyThread 1
wait
Thread 2
fetch
fetch processBody
44
45. other tasks
Clojures are executed sequentially
● Does it mean that ParSeq is single threaded?
● No
other tasks
wait
processBodyThread 1
wait
Thread 2
time
fetch
happens
before
fetch processBody
happens
before
45
46. Clojures are executed sequentially
● In order to perform well ParSeq requires
developer’s cooperation. In clojures avoid:
○ blocking e.g. JDBC calls, Thread.sleep(), ...
○ CPU intensive operations
46
47. Blocking
● Blocking API e.g. Voldemort, JDBC
● CPU intensive computation
● Offload operation to external Executor
Task.blocking(String name,
Callable<? extends T> callable,
Executor executor)
● Supports multiple executors
● Executors management is outside ParSeq 47
48. Automatic cancellation
● Task abstraction borrows ideas from functional
programming, think: Task ~ function call
○ Task is lazy, only runs when needed
○ Task is immutable
○ It’s purpose is to calculate a result
○ Task runs at most once
○ Once result is known, everything that is still not
completed (for some reason) can be cancelled because
it can’t affect result
48
49. Automatic cancellation
Task<String> google = fetchBody("http://www.google.com");
Task<String> yahoo = fetchBody("http://www.yahoo.com");
Task<String> bing = fetchBody("http://www.~#fdcm x 0eirw.com");
Task<Integer> sumLengths =
Task.par(google.map("length", String::length),
yahoo.map("length", String::length),
bing.map("length", String::length))
.map("sum", (g, y, b) -> g + y + b);
49
51. Tracing
● Trace is information about what happened
after calling Engine.run()
● Tracing is always enabled for all tasks
● Allows reasoning about what happened
● Includes timing information, exceptions, etc.
● Obtain trace JSON at any time by calling
Task.getTrace().toString()
● Paste JSON at go/tracevis to visualize 51
52. Tracing best practices
● Add short, meaningful description to every
task
HttpClient.get(url).task()
.map("getBody", response -> response.getResponseBody())
.map("length", s -> s.length());
52
53. Tasks Fusion
● Sequence of synchronous transformations
can be optimized and executed as a chain of
method calls
Task<Integer> length =
HttpClient.get("http://www.google.com").task()
.map("getBody", Response::getResponseBody)
.map("length", s -> s.length());
53
54. Batching
● Allows automatic batching of individual
asynchronous operations without affecting
code readability
54
55. Unit testing
● Use BaseEngineTest as a base class for a
unit test
○ Initialization and shutdown of Engine
○ Lot of helper methods
● Log trace for the tested Task
55
56. Best Practices
● Focus on readable, clean code
○ Split code into small, unit-testable methods that may
return Task<T>
○ Prefer method handles over lambdas
○ Limit length of lambda code blocks to few lines
○ Don’t mix functional APIs inside lambda code block
e.g. Java Stream map() and Java Optional map()
with ParSeq map()
56
57. Upcoming feature
● Automatic batching of GET / BATCH_GET
requests in ParSeqRestClient
● Configurable timeouts in ParSeqRestClient
● Automatic logging traces of failed plans
● Sending sample of traces to kafka for
analysis e.g. finding bottlenecks,
regressions, thread blocking, …
● ParSeq “Collections” 57
waiting is eliminated, threads are busy
note 1: two threads are involved in execution
note 2: req1, req2, process response 1, process response 2 execute sequentially
waiting is eliminated, threads are busy
note 1: two threads are involved in execution
note 2: req1, req2, process response 1, process response 2 execute sequentially