Cooperative¶
This is TIMEx’s default strategy: you promise to peek at the clock now and then, and TIMEx promises not to surprise you with magic interrupts.
Mental model: a hike where you choose every safe rest stop. The trail
(deadline.check!) is where you look at your watch. If you never stop, nobody
pulls you off the path—you just might finish late.
Quick example¶
TIMEx.deadline hands you a Deadline as d. Sprinkle check! inside
loops you own. When time is up, the next check! raises TIMEx::Expired.
The strategy also runs a final check! after your block returns, so a long
non-cooperative tail still surfaces as Expired instead of silently overrunning.
At a glance¶
| Topic | Plain English |
|---|---|
| CPU-heavy work | Only stops at your check! calls (or turn on auto-check if you cannot edit the loop). |
| Blocking IO | Does not cut off a stuck read—reach for IO or Closeable. |
| Mutexes and shared state | Friendly: no random thread exceptions mid-update. |
| Runs everywhere | MRI, no extra gems—this is the boring portable choice. |
| How “tight” the timeout feels | As fine as you make your checkpoints. |
| Nesting | Yes—combine budgets with Deadline#min. |
| Runtime cost | Basically free between checkpoints. |
One sharp edge: rescue Exception¶
TIMEx::Expired subclasses Exception, not StandardError, so a
normal rescue => e will not swallow it. Good.
A wide rescue Exception will catch it on purpose. If legacy code might do
that, wrap the work in TwoPhase so a
cooperative phase still gets a hard backstop:
TIMEx::Composers::TwoPhase.new(
soft: :cooperative, hard: :unsafe, grace: 0.5, hard_deadline: 1.0,
idempotent: true
).call(deadline: 2.0) { legacy_block }
bin/timex-lint nags about bare rescue and rescue Exception inside
TIMEx.deadline blocks—listen to it.
Real-world: nightly Sidekiq export that yields before SIGTERM¶
Sidekiq workers get ~25 s of grace on shutdown. A nightly export that walks
User.find_each in batches needs to finish a batch and stop before the
grace window closes—otherwise the worker dies mid-upload and tomorrow’s job
re-processes the same rows. check! between batches is enough:
class NightlyExportJob
include Sidekiq::Job
def perform(export_id)
TIMEx.deadline(20.0) do |deadline|
User.find_each(batch_size: 500) do |user|
deadline.check!
ExportRow.upsert(user.attributes, export_id: export_id)
end
end
rescue TIMEx::Expired
NightlyExportJob.perform_in(1.minute, export_id)
end
end
The check! lands at safe places (between rows, no half-written upsert), and
the rescue re-enqueues so progress resumes cleanly on the next worker.