
Durable, transactional scheduler recording timers inside caller transactions, emitting "it is time" events (seconds precision), ensuring at-least-once delivery with idempotency keys and lateness tracking.
a durable timer for Kotlin, and nothing else — schedule(id, at, payload), and at at (or
later, but never earlier and never lost) the fact "it is time" leaves the library
⏱ one row in your transaction → one event you cannot lose
Built around a single question: what happens to the timer if the process dies before it fires.
A state change that has to be undone in three days, a confirmation that must expire whether or not anybody comes back, a plan that ends on a date. Written the obvious way, each becomes a row with a timestamp and a job that scans for it — and the scan is written again in every service, subtly differently, and none of them survives being run twice.
The awkward part is not the scanning. It is that the timer and the state change are two writes. If the state is committed and the timer is not, nothing looks wrong: the operation succeeded, the row is correct, and only the thing nobody is watching for never happens.
chronik writes the timer in the caller's transaction, so the two commit together or not at all.
repositories {
mavenCentral()
}
dependencies {
implementation("io.github.youndie.chronik:chronik-core:0.1.0")
implementation("io.github.youndie.chronik:chronik-postgres:0.1.0")
}Releases are on Maven Central. Snapshots keep going to
https://reposilite.kotlin.website/snapshots as 0.1.0.<build>, which is where to look for
something merged but not released — add that repository beside mavenCentral() to take one.
Two backends, and which one you can use depends on where you run. chronik-postgres is the JVM
only — it is Exposed and JDBC. chronik-sqlx4k-sqlite is SQLite through sqlx4k and is built for the JVM
and for linuxX64, as are chronik-core and chronik-conformance, so a Kotlin/Native service has
a store rather than only a primitive.
// a Kotlin/Native service, on the driver it already opened
implementation("io.github.youndie.chronik:chronik-sqlx4k-sqlite:0.1.0")chronik-sqlx4k-sqlite carries no driver: it takes the sqlx4k Driver your application opened, and states
the schema it needs (chronikTimersSchema()) for your own migrations to run.
chronik-postgres ships no driver, no connection pool and no DDL: it takes an Exposed Database
you hand it, and the table describes itself — indexes included — so a schema generator produces
something that matches what the queries actually filter on.
Every one of these is a deliberate refusal with a numbered decision behind it, not a gap:
Seconds. Not milliseconds, and this is written down rather than implied: millisecond precision needs a different wake-up mechanism than polling, and a promise that polling cannot keep is worse than no promise.
Lateness — how long after its due time a timer actually fired — is a supported outcome rather than an error, and it is the number this project publishes. A timer whose moment passed while the process was down fires on start-up, and the lateness is recorded. Delivery latency on its own cannot tell "fired on time" from "picked up forty minutes late".
No figure is quoted here yet, because none has been measured.
a durable timer for Kotlin, and nothing else — schedule(id, at, payload), and at at (or
later, but never earlier and never lost) the fact "it is time" leaves the library
⏱ one row in your transaction → one event you cannot lose
Built around a single question: what happens to the timer if the process dies before it fires.
A state change that has to be undone in three days, a confirmation that must expire whether or not anybody comes back, a plan that ends on a date. Written the obvious way, each becomes a row with a timestamp and a job that scans for it — and the scan is written again in every service, subtly differently, and none of them survives being run twice.
The awkward part is not the scanning. It is that the timer and the state change are two writes. If the state is committed and the timer is not, nothing looks wrong: the operation succeeded, the row is correct, and only the thing nobody is watching for never happens.
chronik writes the timer in the caller's transaction, so the two commit together or not at all.
repositories {
mavenCentral()
}
dependencies {
implementation("io.github.youndie.chronik:chronik-core:0.1.0")
implementation("io.github.youndie.chronik:chronik-postgres:0.1.0")
}Releases are on Maven Central. Snapshots keep going to
https://reposilite.kotlin.website/snapshots as 0.1.0.<build>, which is where to look for
something merged but not released — add that repository beside mavenCentral() to take one.
Two backends, and which one you can use depends on where you run. chronik-postgres is the JVM
only — it is Exposed and JDBC. chronik-sqlx4k-sqlite is SQLite through sqlx4k and is built for the JVM
and for linuxX64, as are chronik-core and chronik-conformance, so a Kotlin/Native service has
a store rather than only a primitive.
// a Kotlin/Native service, on the driver it already opened
implementation("io.github.youndie.chronik:chronik-sqlx4k-sqlite:0.1.0")chronik-sqlx4k-sqlite carries no driver: it takes the sqlx4k Driver your application opened, and states
the schema it needs (chronikTimersSchema()) for your own migrations to run.
chronik-postgres ships no driver, no connection pool and no DDL: it takes an Exposed Database
you hand it, and the table describes itself — indexes included — so a schema generator produces
something that matches what the queries actually filter on.
Every one of these is a deliberate refusal with a numbered decision behind it, not a gap:
Seconds. Not milliseconds, and this is written down rather than implied: millisecond precision needs a different wake-up mechanism than polling, and a promise that polling cannot keep is worse than no promise.
Lateness — how long after its due time a timer actually fired — is a supported outcome rather than an error, and it is the number this project publishes. A timer whose moment passed while the process was down fires on start-up, and the lateness is recorded. Delivery latency on its own cannot tell "fired on time" from "picked up forty minutes late".
No figure is quoted here yet, because none has been measured.