Kotlin allows defining domain-specific languages through type-safe builders⇗.
Each DSL expression creates a scope as the receiver, which allows to call additional expressions.
job, async, asyncJob, retry are io.turbodsl functions that create scopes allowing to include any Kotlin expression, or more nested io.turbodsl expressions.
This range of io.turbodsl expressions can be chained together to execute synchronous or asynchronous blocks, each one receiving an input as an argument, returning an output.
Specific annotations using @DslMarker⇗ has been included so IDE's can highlight accordingly.
RootScope defines the input and name.
RuntimeScope defines attributes related to delay, timeout, output, default, and context (Kotlin CoroutineContext).
SyncScope defines functions to create other scopes.
TurboScope is the main-entry scope.
JobScope represents a synchronous-job.
AsyncJobScope represents an asynchronous-job.
AsyncScope allows to register AsyncJobScopes and execute them.
AsyncRawScope can register any number of AsyncJobScopes.
AsyncReturnScope can register any number of AsyncJobScopes, but also exposes simple mechanisms to execute 2 (up to 10) AsyncJobScopes.
All Collection⇗ scopes extend AsyncScope.
AsyncResultScope is created internally to process results coming from an AsyncReturnScope.
RetryScope manages retry patterns.
Additional scopes will be added in the future to expand functionality and include new features.
TurboScope.execute<Unit> {
// "this" is an instance of TurboScope
job {
// "this" is an instance of JobScope
}
async {
// "this" is an instance of AsyncRawScope
asyncJob { // registers an async-job
// "this" is an instance of AsyncJobScope
}
}
async<Unit>(
build = {
// "this" is an instance of AsyncReturnScope
asyncJob { ... } // registers an async-job
asyncJob { ... } // registers an async-job
}
) { ok, r ->
// "this" is an instance of AsyncResultScope
}
async(
job1 = asyncJob<Int> { ... }, // creates an async-job
job2 = asyncJob<String> { ... }, // creates an async-job
:
job10 = asyncJob<Boolean> { ... }, // creates an async-job
) { ok, r1, r2, ..., r10 ->
// "this" is an instance of AsyncResultScope
}
}
// `execute` creates a TurboScope allowing to use
// other DSL expressions since it inherits all
// functionality from SyncScope
TurboScope.execute {
// This block executes synchronously.
job {
// This block executes synchronously.
}
async {
// This block executes synchronously.
asyncJob { // Registers an AsyncJobScope
// This block executes synchronously when
// the parent `async` decides.
}
asyncJob { // Registers an AsyncJobScope
// This block executes synchronously when
// the parent `async` decides.
}
// Once the execution of this block is completed,
// `async` will execute concurrently all the
// registered AsyncJobScopes.
}
// `execute` will return List<AsyncResult<*>> since
// async is the last expression, as inferred by
// Kotlin compiler.
}
TurboScope.execute is the entry point which can be used anywhere within your Kotlin codebase.
It launches a coroutine, but it will wait for its complete execution.
Each scope defines functions to create another scope, allowing to chain and nest expressions idiomatically.
There are two scopes defining how your code gets executed:
SyncScope executes everything synchronously, just as usual Kotlin expressions.
AsyncScope allows to register AsyncJobScopes for parallel execution.
See Sync vs. Async Scopes⇗.
Every RootScope receives an input parameter (generic I):
This input can be declared explicitly, or inferred by the compiler. If input is not required, it will be Unit .
If the scope does not define it, it will implicitly receive such input from its parent.
Similarly, every RuntimeScope has an output (generic O):
You can specify it explicitly or let Kotlin infer its data-type.
If output is not required, it will be Unit .
If a default is specified, it will be returned if the scope fails its execution. This provides a fail-safe mechanism when required.
Note that most IDE's can provide hints about the inferred type.
TurboScope.execute(
default = Foobar("foo").toDefault()
) {
job<Foobar>(
defaultFun = {
// Only executed when scope fails
Foobar("bar").toDefault()
}
) { ... }
}
Every scope accepts default values in case its execution fails.
Two different default mechanisms can be specified:
default → A value that should be returned on failures.
defaultFun → A lambda that should be executed on failures.
See Default Mechanisms⇗ for more details.
Every scope can define a timeout to limit its own execution.
This mechanism is applied across the entire scope hierarchy:
Parent-scope timeout limits the total execution time for all its children.
A child-scope can define a timeout lower than its parent's.
If a timeout is detected and no default is specified, ScopeTimeoutException is thrown.
TurboScope.execute<Unit>(timeout = 2_000L) {
async(
timeout = 1_000L,
job1 = asyncJob<Int>(timeout = 10L) { 10 },
job2 = asyncJob<Double>(timeout = 10L) { 1.0 },
job3 = asyncJob<Foobar>(timeout = 10L) {
Foobar("bar")
},
) { ok, r1, r2, r3 ->
// process results
}
}
TurboScope.execute<Unit>(
delay = 1_000L,
timeout = 500L
) {
// Waits for 1 second before executing.
// The maximum time to complete this block
// is 0.5 seconds
job(delay = 1_000L) {
// This will fail since this job will wait
// 1 second before executing since the
// parent timeout is 0.5 seconds.
}
}
An initial delay can be specified on any scope.
While not required, there may be situations you may want to add a slight delay prior to a scope execution.
Note that this delay is not considered within the timeout definition of the scope.
Notice that if any child-scope includes a delay, such time will be counted on the parent's timeout calculation.
async expression is used to declare and execute asynchronous-jobs (aka AsyncJobs).
There are 3 different ways to do this:
Using async where all results are returned as a List<AsyncResult<*>>. This provides more flexibility on how you handle the results.
Using async with a build lambda. This is similar to the previous technique, but allows you to have an AsyncResultScope, which provides useful expressions to analyze the results.
Finally, using async to include between 2 and 10 jobs as arguments (job1, job2, ..., job10). The related AsyncResultScope will receive each result as an argument, simplifying how you process all the results.
Note that there are different asynchronous execution modes.
For more details see:
TurboScope.execute<Unit> {
async {
asyncJob { 1 }
asyncJob { 2 }
// register more jobs as required
}.let { r ->
// async returns a list
}
async<Unit>(
build = {
asyncJob { 1 }
asyncJob { 2 }
// register more jobs as required
}
) { ok, r ->
// if ok==true, r contains all successes
}
async(
job1 = asyncJob<Int>{ 1 },
job2 = asyncJob<String>{ "2" },
job3 = asyncJob<Foobar>{ Foobar("bar")},
// register up to 10 jobs as required
) { ok, r1, r2, r3 ->
// if ok==true, r1, r2, r3 are successes
// `r1` contains job1 result as Int
// `r2` contains job2 result as String
// `r3` contains job3 result as Foobar
}
}
TurboScope.execute(timeout = 500L) {
retry<String>(
retryMode = RetryScope.RetryMode.OnErrorOnly,
retries = 3, // retry 3 more times
retryDelay = 10L, // wait between retries
default = "NONE".toDefault(),
) {
// This block could be executed 4 times.
:
// If the last retry fails, return default.
}
}
retry allows to execute any arbitrary block of code several times, without having to include additional statements or expressions.
Since RetryScope extends SyncScope, it supports input, timeout, delay, output, and default.
Additional attributes can be specified, including different retry-modes to handle errors.
See Retry Pattern⇗ for more details.