refactor(scheduler)!: typed TaskId, LocalTime, @Serializable inputData#204
Merged
kdroidFilter merged 15 commits intomainfrom Apr 17, 2026
Merged
refactor(scheduler)!: typed TaskId, LocalTime, @Serializable inputData#204kdroidFilter merged 15 commits intomainfrom
kdroidFilter merged 15 commits intomainfrom
Conversation
- Replace String taskId with @JvmInline value class TaskId, validated against [a-zA-Z0-9_-]+ in init. Threads through TaskRequest, registry, context, info, all backends, metadata store and wrapper scripts. - CronExpression factory methods now take java.time.LocalTime instead of raw (hour, minute) ints; validation comes for free from LocalTime. - Replace string-keyed TaskData with an opaque JSON wrapper backed by kotlinx.serialization. New API: inputData(value) reified inline on TaskRequest.Builder, context.inputData<T>() on TaskContext, with explicit-serializer overloads. Persistence consolidates to a single _inputDataJson key in the per-task .properties file. - Demo and scheduler-testing tests rewritten against the new typed API; mkdocs scheduler page updated. Breaking change for any consumer still passing String IDs, raw hour/minute ints to CronExpression, or the old TaskData.Builder/getString/getInt API.
…esult - TaskResult.Failure(message) and TaskResult.Retry(message) lose their default-null message: callers must pass a String. Removes the silent "unknown" fallback and the temptation to skip explanations. - New LastTaskResult sealed @serializable interface with Success / Failure / Retry / ConstraintsNotMet variants. - TaskInfo.lastResult: String? -> LastTaskResult? — consumers now pattern-match instead of parsing free-form strings. - TaskMetadataStore stores the last result as a JSON-encoded LastTaskResult under _lastResult; recordConstraintSkip gains an incrementAttempt flag so calendar/on-boot constraint failures bump the attempt counter while keeping the typed ConstraintsNotMet payload. - TestDesktopTaskScheduler in-memory metadata mirrors the new typed model. - Tests, demo and mkdocs page updated. Breaking for any consumer that called TaskResult.Failure() / .Retry() with no message, or that read TaskInfo.lastResult as a String.
…ix Constraints doc - Avoid the property/extension shadowing on TaskContext (where context.inputData returned a TaskData while context.inputData<T>() returned T?). The wrapper is now exposed as rawInputData; the typed inputData<T>() / inputData(serializer) extensions stay the canonical read path. - Fix the Constraints API reference table that listed a non-existent requiresStorageNotLow: Boolean. The actual constraint is minimumStorageBytes: Long? (already correctly described in the usage sections).
…add UPDATE_DATA - KEEP becomes a strict no-op on every backend. Previously Linux already did this, but macOS and Windows silently overwrote the persisted inputData / constraints / taskType — surprising behavior for callers enqueuing on every app start. - New ExistingTaskPolicy.UPDATE_DATA covers the previous macOS/Windows behavior explicitly: keep the OS-level schedule, refresh the persisted metadata. - Each backend now factors the metadata-write into a private persistMetadata(request) helper so KEEP / UPDATE_DATA / REPLACE branch cleanly. - TestDesktopTaskScheduler mirrors the new tri-state policy and gets a test covering UPDATE_DATA. - Doc: dedicated runImmediately section (covers constraint interaction and the no-op-on-calendar/onBoot detail), Existing-task-policy table rewritten, periodic-tasks section spells out the IllegalArgumentException thrown when interval < 15 minutes.
Asymmetric API — Monday had a shorthand but the other six days didn't. Use everyWeekdayAt(DayOfWeek.MONDAY, time) instead.
… and co-manage TestConstraintChecker
- ExecutionRecord.result is now LastTaskResult instead of TaskResult.
Constraint-skipped fires (periodic AND calendar/onBoot) now produce a
Record with LastTaskResult.ConstraintsNotMet — previously periodic
skips were invisible in execution history and calendar/onBoot skips
carried a synthetic TaskResult.Retry.
- runTask() returns null for both periodic and calendar/onBoot constraint
skips (was: null vs synthetic Retry). Calendar/onBoot still bumps
runAttemptCount to mirror prod retry semantics.
- advanceTimeBy() now includes records for skipped fires.
- TestDesktopTaskScheduler constructor takes a TestConstraintChecker?,
whose install/uninstall are co-managed by install() / close(). The
earlier two-step setup (separate install for the checker, mutable
property assignment after install) collapses into a single use { }.
- Doc: minimum-interval gets its own anchor so the platform-support
table links to the exact paragraph; testing-constraints example
rewritten for the co-managed lifecycle, with an admonition that
ConstraintChecker is @InternalSchedulerApi (test seam only, not for
production gating).
…ws) and launchd behavior
…rify everyHour() semantics
…rification Earlier text claimed launchd 'fails the load and stops trying' and that the .plist gets reclaimed when Application Support is removed. Both wrong: - launchd retries forever, throttled by ThrottleInterval (default 10s), spamming 'cannot spawn' into system.log on every attempt. - ~/Library/LaunchAgents/ orphans are never auto-cleaned by macOS. Doc now spells out the active cleanup an uninstaller should perform: 'launchctl bootout gui/$(id -u) <plist>' followed by 'rm <plist>'.
…on, surface cancelAll() as in-app cleanup primitive
…doc as precedent for manual plist removal
Verified against help.dropbox.com — Dropbox's official uninstall doc does NOT mention LaunchAgents or plist cleanup. The 'manual plist removal' pattern only appears in third-party uninstall guides, not the apps' own documentation. Reframed as an unattributed ecosystem limitation.
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Summary
Three breaking refactors of the public scheduler API for stronger typing.
@JvmInline value class TaskId(val value: String)with regex validation ininit. ReplacesStringtaskId acrossTaskRequest,TaskRegistry,TaskContext,TaskInfo,PlatformScheduler, all 4 backends (Linux/macOS/Windows/Noop),TaskMetadataStore,TaskWrapperScript,DesktopBootReceiver, andscheduler-testing. Raw strings are unwrapped only at JNI/filesystem boundaries; the boot-receiver rejects malformed CLI IDs before any lookup.(hour: Int, minute: Int)overloads. NoweveryDayAt(LocalTime),everyWeekdayAt(LocalTime),everyWeekdayAt(DayOfWeek, LocalTime),everyMondayAt(LocalTime),everyHour(). Bounds checks come fromLocalTimeitself.@SerializableinputData — dropped string-keyedTaskData.Builder.putString/putInt/....TaskDatais now an opaque wrapper around a JSONString?. New API:TaskRequest.Builder.inputData(value)reified inline (and an explicit-serializer overload),context.inputData<T>()to decode.TaskMetadataStorepersists the payload in a single reserved_inputDataJsonkey.kotlinx-serialization-jsonis added asapitoschedulerso consumers don't have to wire it themselves; the kotlinx serialization plugin is enabled inscheduler,scheduler-testing, andscheduler-demo. Demo (SampleTasks,SchedulerDemoView) and tests rewritten against the new API.docs/runtime/scheduler.mdrewritten to match.Breaking changes
StringIDs toenqueue/cancel/isScheduled/getTaskInfo, registering withregister("foo") { ... }, or building requests withTaskRequest.periodic("foo", ...)must wrap inTaskId(...).CronExpression.everyDayAt(9)/everyWeekdayAt(18)must switch toLocalTime.of(...).context.inputData.getString("k")/ writinginputData { putString("k", "v") }must define a@Serializabledata class and switch toinputData(value)/context.inputData<T>().<taskId>.propertiesfiles with arbitrary keys are ignored on next run (only_inputDataJsonand the underscore-prefixed bookkeeping keys are read). No migration shim — assumed acceptable for an unreleased module.Test plan
./gradlew :scheduler:compileKotlin :scheduler:compileTestKotlin :scheduler-testing:compileTestKotlin :scheduler-demo:compileKotlin— green./gradlew :scheduler:test :scheduler-testing:test— greenscheduler-demoon Linux/Windows/macOS, verify it fires andcontext.inputData<BackupInput>()decodes the payloadmkdocs serverenders the updatedscheduler.mdcleanly