-
Notifications
You must be signed in to change notification settings - Fork 8
feat(backend): ensure a task is only executed once regardless of how many backend instances are run #6711
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
anna-parker
wants to merge
24
commits into
main
Choose a base branch
from
shedLock_alternative
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
feat(backend): ensure a task is only executed once regardless of how many backend instances are run #6711
Changes from all commits
Commits
Show all changes
24 commits
Select commit
Hold shift + click to select a range
5e00b07
feat(backend): ensure a task is only executed once regardless of how …
anna-parker fca9ab8
Update schema documentation based on migration changes
actions-user 77c3fc9
deflake
anna-parker 6891d8b
fixup
anna-parker a05d68c
Update schema documentation based on migration changes
actions-user 3909681
add lockFraction
anna-parker 75dd491
format
anna-parker a429747
feat(backend): allow early release of the lock
anna-parker 1f00b6f
Update schema documentation based on migration changes
actions-user 5ec1cf5
clean up
anna-parker 49f21fd
use claude to apply claude's suggestions
anna-parker b95ac62
format
anna-parker 5de465c
fixup
anna-parker 698431c
remove default
anna-parker 044b78f
Update schema documentation based on migration changes
actions-user bea2bd8
make a service
anna-parker 88a65b1
fix tests
anna-parker 2c70ea3
feat: format
anna-parker ec337cb
clean up
anna-parker e326bc7
Update backend/src/main/kotlin/org/loculus/backend/service/scheduler/…
anna-parker 62826f3
lint
anna-parker a0d3c7d
wuppsi
anna-parker 7643a00
chore(backend): add task lock annotation (#6728)
corneliusroemer 55c2954
wuppsi
anna-parker File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
7 changes: 7 additions & 0 deletions
7
backend/src/main/kotlin/org/loculus/backend/service/scheduler/TaskLock.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| package org.loculus.backend.service.scheduler | ||
|
|
||
| import java.util.concurrent.TimeUnit | ||
|
|
||
| @Target(AnnotationTarget.FUNCTION) | ||
| @Retention(AnnotationRetention.RUNTIME) | ||
| annotation class TaskLock(val name: String, val intervalString: String, val timeUnit: TimeUnit = TimeUnit.SECONDS) |
40 changes: 40 additions & 0 deletions
40
backend/src/main/kotlin/org/loculus/backend/service/scheduler/TaskLockAspect.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| package org.loculus.backend.service.scheduler | ||
|
|
||
| import org.aspectj.lang.ProceedingJoinPoint | ||
| import org.aspectj.lang.annotation.Around | ||
| import org.aspectj.lang.annotation.Aspect | ||
| import org.springframework.context.EmbeddedValueResolverAware | ||
| import org.springframework.stereotype.Component | ||
| import org.springframework.util.StringValueResolver | ||
|
|
||
| @Aspect | ||
| @Component | ||
| class TaskLockAspect(private val taskLockService: TaskLockService) : EmbeddedValueResolverAware { | ||
| private lateinit var embeddedValueResolver: StringValueResolver | ||
|
|
||
| override fun setEmbeddedValueResolver(resolver: StringValueResolver) { | ||
| embeddedValueResolver = resolver | ||
| } | ||
|
|
||
| @Around(value = "@annotation(taskLock)", argNames = "joinPoint,taskLock") | ||
| fun lockTask(joinPoint: ProceedingJoinPoint, taskLock: TaskLock): Any? { | ||
| val intervalSeconds = taskLock.timeUnit.toSeconds(resolveInterval(taskLock)) | ||
| if (!taskLockService.acquireLock(taskLock.name, frequencyIntervalSeconds = intervalSeconds)) return null | ||
|
|
||
| try { | ||
| return joinPoint.proceed() | ||
| } finally { | ||
| taskLockService.releaseLock(taskLock.name, frequencyIntervalSeconds = intervalSeconds) | ||
| } | ||
| } | ||
|
|
||
| private fun resolveInterval(taskLock: TaskLock): Long { | ||
| val resolvedInterval = embeddedValueResolver.resolveStringValue(taskLock.intervalString) | ||
| ?: throw IllegalArgumentException("Could not resolve lock interval for task '${taskLock.name}'") | ||
|
|
||
| return resolvedInterval.toLongOrNull() | ||
| ?: throw IllegalArgumentException( | ||
| "Lock interval for task '${taskLock.name}' must resolve to a whole number, but was '$resolvedInterval'", | ||
| ) | ||
| } | ||
| } |
111 changes: 111 additions & 0 deletions
111
backend/src/main/kotlin/org/loculus/backend/service/scheduler/TaskLockService.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,111 @@ | ||
| package org.loculus.backend.service.scheduler | ||
|
|
||
| import mu.KotlinLogging | ||
| import org.jetbrains.exposed.sql.LongColumnType | ||
| import org.jetbrains.exposed.sql.TextColumnType | ||
| import org.jetbrains.exposed.sql.statements.StatementType | ||
| import org.jetbrains.exposed.sql.transactions.transaction | ||
| import org.springframework.beans.factory.annotation.Value | ||
| import org.springframework.stereotype.Service | ||
|
|
||
| private val log = KotlinLogging.logger {} | ||
|
|
||
| const val TASK_LOCK_TABLE_NAME = "task_lock" | ||
|
|
||
| /** | ||
| * Lock can be acquired when current time > `locked_until` (or no row for this task exists) | ||
| * When acquiring, `locked_until` is set to `currentTime + maxLockFactor * frequencyIntervalSeconds` | ||
| * When releasing, `locked_until` is reduced to `started_at + minLockFactor * frequencyIntervalSeconds` | ||
| * maxLockFactor prevents concurrent execution even if tasks run longer than the interval | ||
| * minLockFactor prevents multiple backends from starting new tasks before the interval is almost over | ||
| */ | ||
| @Service | ||
| class TaskLockService( | ||
| @Value("\${loculus.task-lock.min-lock-factor:0.9}") private val minLockFactor: Double, | ||
| @Value("\${loculus.task-lock.max-lock-factor:5.0}") private val maxLockFactor: Double, | ||
| ) { | ||
|
|
||
| /** | ||
| * Attempts to acquire a lock for the given task. | ||
| * | ||
| * If the task dies or is terminated, the lock will be released after [maxDuration] seconds. | ||
| * | ||
| * @param taskName unique name identifying the task. | ||
| * @param maxDuration maximum duration for which to hold the lock in seconds. | ||
| * @return true if the lock was acquired, false if another instance holds it. | ||
| */ | ||
| fun acquireLock(taskName: String, frequencyIntervalSeconds: Long): Boolean = transaction { | ||
| val maxDuration = (frequencyIntervalSeconds * maxLockFactor).toLong() | ||
| val acquired = exec( | ||
| """ | ||
| WITH lock_attempt AS ( | ||
| INSERT INTO task_lock (task_name, started_at, locked_until) | ||
| VALUES (?, NOW(), NOW() + (? * interval '1 second')) | ||
| ON CONFLICT (task_name) DO UPDATE | ||
| SET started_at = NOW(), locked_until = NOW() + (? * interval '1 second') | ||
| WHERE task_lock.locked_until <= NOW() | ||
| RETURNING task_name | ||
| ) | ||
| SELECT COUNT(*) FROM lock_attempt | ||
|
anna-parker marked this conversation as resolved.
|
||
| """.trimIndent(), | ||
| args = listOf( | ||
| TextColumnType() to taskName, | ||
| LongColumnType() to maxDuration, | ||
| LongColumnType() to maxDuration, | ||
| ), | ||
| // The CTE starts with INSERT, so Exposed would default to StatementType.INSERT and | ||
| // not return a ResultSet. Overriding to SELECT lets us read the outer COUNT(*). | ||
| explicitStatementType = StatementType.SELECT, | ||
| ) { rs -> | ||
| rs.next() && rs.getLong(1) > 0L | ||
| } ?: false | ||
|
|
||
| if (!acquired) { | ||
| log.debug { | ||
| "Task '$taskName' skipped: another replica acquired the lock within the last ${maxDuration}s" | ||
| } | ||
| } | ||
| acquired | ||
| } | ||
|
|
||
| /** | ||
| * Attempts to "release" a lock for the given task, this is only possible if the | ||
| * lock is considered expired based on the [minLockFactor]. | ||
| * If locked_until is still in the future update locked_until to the minimum duration, otherwise do nothing. | ||
| * | ||
| * @param taskName unique name identifying the task. | ||
| */ | ||
| fun releaseLock(taskName: String, frequencyIntervalSeconds: Long) = transaction { | ||
| // The effective lock duration is shortened by [minLockFactor] to prevent tasks | ||
| // from being blocked after their scheduled interval due to minor clock skew, | ||
| // execution delays, or lock acquisition latency. | ||
| val minDuration = (frequencyIntervalSeconds * minLockFactor).toLong() | ||
|
|
||
| val updated = exec( | ||
| """ | ||
| UPDATE task_lock | ||
| SET locked_until = started_at + (? * interval '1 second') | ||
| WHERE task_name = ? | ||
| AND locked_until > NOW() | ||
| RETURNING task_name | ||
| """.trimIndent(), | ||
| args = listOf( | ||
| LongColumnType() to minDuration, | ||
| TextColumnType() to taskName, | ||
| ), | ||
| explicitStatementType = StatementType.SELECT, | ||
| ) { rs -> | ||
| rs.next() | ||
| } ?: false | ||
|
|
||
| if (updated) { | ||
| log.debug { | ||
| "Task '$taskName' lock: 'locked_until' shortened to minimum duration (${minDuration}s)" | ||
| } | ||
| } else { | ||
| log.debug { | ||
| "Task '$taskName' lock: not shortened because 'locked_until' has already elapsed" | ||
| } | ||
| } | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
7 changes: 7 additions & 0 deletions
7
...in/kotlin/org/loculus/backend/service/submission/CleanUpStaleSequencesInProcessingTask.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.