Cancellation Token¶
Sometimes you need a loud “stop, please” flag that many threads can read, and
a few friendly callbacks when the flag flips. That is
TIMEx::CancellationToken: a thread-safe, one-shot cancel switch with
observer hooks.
Composers like Hedged, plus the Wakeup strategy,
use this under the hood. You can also use it directly when your code owns
the lifecycle and you want the same “cancel + reason” vocabulary.
Quick example¶
token = TIMEx::CancellationToken.new
token.on_cancel do |reason|
release_resources(reason)
end
# Somewhere else in the app
token.cancel(reason: :user_aborted)
token.cancelled? # => true
token.reason # => :user_aborted
Rules of the road¶
| Behavior | Plain English |
|---|---|
| Observers registered after cancel | They still run—TIMEx does not leave new listeners hanging. |
Second cancel |
Idempotent: returns false, does not spam callbacks again. |
reason |
Optional symbol or object so teardown code knows why life ended. |
Think of it as a tiny pub/sub for “we are done here,” without inventing your own mutex soup.
When to reach for it¶
- You are threading cancellation through your layers and want one shared object.
- You are composing TIMEx pieces and need the same semantics the built-in strategies expect.
If you only need “stop this TIMEx block,” a Deadline plus check! is
usually simpler—tokens shine when cancellation is orthogonal to the time
budget.
Real-world: user clicks “Cancel export”¶
A CSV export streams rows into S3. A producer thread reads from the DB while
an uploader thread pushes parts. When the user hits Cancel in the UI, the
controller flips one token—both threads notice on their next loop iteration,
and the on_cancel hook logs why so support has a trail:
token = TIMEx::CancellationToken.new
token.on_cancel { |reason| Rails.logger.info("export aborted: #{reason}") }
producer = Thread.new do
User.find_each(batch_size: 500) do |user|
break if token.cancelled?
queue << UserExportRow.from(user)
end
end
uploader = Thread.new do
multipart = S3.start_multipart(bucket: "exports", key: export_id)
until token.cancelled? || queue.empty?
multipart.upload_part(queue.pop)
end
token.cancelled? ? multipart.abort : multipart.complete
end
# In the controller action that handles DELETE /exports/:id
token.cancel(reason: :user_aborted)
Two unrelated threads, one switch, predictable teardown—no Thread#kill,
no half-uploaded zombie part lingering in S3.