A unit-of-work could potentially be retried if the expected operation may experience transient failures:
Unhandled errors / exceptions
Time-based conditions
IO operations (network, file system)
io.turbodsl provides retry mechanisms through a RetryScope:
retry {...} → retry any arbitrary block of code.
This expressions accept the following attributes:
retries → Number of additional executions - default: 1
Negative values will throw a ScopeImplementationException.
retryDelay → A delay per retry - default: 0 milliseconds
retryDelayFactor → A factor to apply on retryDelay on each retry attempt:
x=1.0 → retryDelay is the same for each retry - this is the default.
x>1.0 → retryDelay is increased for each retry.
0<x<1.0 → retryDelay is decreased for each retry.
x=0 → retryDelay is applied only on first retry.
-1.0<x<0 → retryDelay is applied for odd-retries only, decreasing for each retry.
x=-1.0 → retryDelay is the same and applied for odd-retries only.
x<-1.0 → retryDelay is applied for odd-retries only, increasing for each retry.
Formula: retryDelay * retryDelayFactor.pow(retryCount - 1)
retryMode → how retries are managed:
OnTimeoutOnly → only retry if a timeout is detected.
OnErrorOnly → only retry if an error other than a timeout is detected.
Always → always retry on any error, even on timeout - this is the default.
Never → never retry - just to simplify enabling/disabling retry mechanisms during development / debug activities.
If last retry fails, a ScopeRetryException is thrown.
// Returns a String
TurboScope.execute {
// Retry 5 times whenever AsyncJobs take more than 2 seconds.
// Any other error will be thrown.
// Delay each retry by 1, 2, 4, 8, 16 seconds respectively.
// retry#1: 1_000L * 2_000L.pow(1 - 1) -> 1_000L
// retry#2: 1_000L * 2_000L.pow(2 - 1) -> 2_000L
// retry#3: 1_000L * 2_000L.pow(3 - 1) -> 4_000L
// retry#4: 1_000L * 2_000L.pow(4 - 1) -> 8_000L
// retry#5: 1_000L * 2_000L.pow(5 - 1) -> 16_000L
// If last retry fails on timeout, return "INVALID"
retry(
retryMode = RetryScope.RetryMode.OnTimeoutOnly,
retries = 5,
timeout = 2_000L,
retryDelay = 1_000,
retryDelayFactor = 2.0,
default = "INVALID".toDefault()
) {
// Any Kotlin expressions / statements
// Or even additional io.turbodsl expressions
job { ... }
job { ... }
:
}
}
AsyncScopes can be retried without problems, retrying the all registered AsyncJobs. Note that using retry {...} within the build phase is not allowed.
Therefore, retry expression has been marked as deprecated and will throw a ScopeImplementationException:
TurboScope.execute {
async {
retry { // deprecated / not-allowed
asyncJob { ... }
asyncJob { ... }
:
}
}
async(
build = {
retry { // deprecated / not-allowed
asyncJob { ... }
asyncJob { ... }
:
}
}
) { ok, r ->
:
}
}
If you need to retry specific a specific AsyncJob, then you must include retry {...} within the AsyncJob implementation:
TurboScope.execute {
async {
// Each asyncJob has different requirements for retry.
asyncJob<SomeType1> {
retry(...) { ... }
}
asyncJob { ... }
asyncJob<SomeType2> {
retry(...) { ... }
}
:
}
async(
build = {
// Each asyncJob has different requirements for retry.
asyncJob<SomeType3> {
retry(...) { ... }
}
asyncJob { ... }
asyncJob<SomeType4> {
retry(...) { ... }
}
:
}
) { ok, r ->
:
}
}
The main reasons for these constraints are related to how AsyncScopes register, schedule, and execute AsyncJobs:
The lambas specified for build should not be retried, otherwise it could potentially affect the overall results.
Attempting to keep track of which AsyncJobs to re-execute adds unnecessary complexity.
Notice that expressions async(job1=..., job2=..., ...) do not expose the "build" phase making them easier to use:
TurboScope.execute<Unit> {
retry(...) {
async(
// Each asyncJob has different requirements for retry.
job1 = asyncJob<SomeType1> { ... },
job2 = asyncJob<SomeType2> { ... },
job3 = asyncJob<SomeType3> {
retry(...) { ... }
},
job4 = asyncJob<String> {
retry(...) { ... }
},
) { ok, r1, r2, r3, r4 ->
:
}
}
}