Skip to content

Commit 98c0d23

Browse files
committed
docs: correct the claim that there is no unschedule
docs/reminders.md said "Solid Objects has no unschedule". The gem has had unschedule and unschedule_all since reminder cancellation was added, and the tests cover them. An agent that read the guide was told that a Rails reminder cannot be cancelled. The guide now has a section on how to cancel a reminder: by operation, by key, or by handle, with the facts that the tests prove. The agent guide shows the cancel call and lists a guessed cancel method as a mistake. A new test fails when the reminder guide does not name a public reminder method of an actor, or denies one.
1 parent 73bb188 commit 98c0d23

4 files changed

Lines changed: 66 additions & 2 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
# Changelog
22

3+
## Unreleased
4+
5+
- Correct `docs/reminders.md`, which said that Solid Objects has no
6+
`unschedule`. The gem has `unschedule` and `unschedule_all`, and the guide
7+
now has a section on how to cancel a reminder. The agent guide shows the
8+
cancel call. A new test fails when the reminder guide does not name a public
9+
reminder method of an actor.
10+
311
## 1.0.0 - 2026-10-13
412

513
- Version 1.0 freezes the public API under semantic versioning.

‎docs/agents.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ Obey these rules in actor code:
169169
- Keep state in `attribute` values. State must be JSON-compatible.
170170
- Use `schedule(at:, key:)` for delayed work. A reminder is one named alarm
171171
for each actor and key. A new `schedule` with the same key moves the alarm.
172+
`unschedule(:expire, key: buyer)` cancels it.
172173
- Use `reject(code, message)` for a business rule failure that must not retry.
173174
It takes a code and a message, for example
174175
`reject(:room_full, "The room is full")`.
@@ -191,6 +192,7 @@ Avoid these mistakes:
191192
| `schedule(at: deadline)` with no operation after it | `schedule(at: deadline, key: buyer).expire(buyer:)`. A `schedule` call with no operation after it fails the turn with `SolidObjects::ScheduleWithoutOperation` |
192193
| `reject "room full"` | `reject(:room_full, "The room is full")` |
193194
| `id` inside an actor | `actor_id`. An actor has no `id` method |
195+
| A guessed cancel method, such as `cancel_reminder` | `unschedule(:expire, key: buyer)`. `unschedule_all(:expire)` cancels every key of one operation |
194196
| `register_effect(:name) { \|context, arguments\| ... }` | `register_effect(:name) { \|arguments, context\| ... }`. The arguments come first |
195197

196198
[Reminders](reminders.md) and the [architecture guide](architecture.md) give

‎docs/reminders.md‎

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,8 +129,35 @@ cannot strand an entry because the handler drains by time rather than by alarm.
129129
Prefer it when the queue is large and the items are interchangeable; prefer
130130
`key:` when an item needs its own alarm that can be moved on its own.
131131

132-
Solid Objects has no `unschedule`. A reminder stops when its handler does not
133-
re-arm it, and destroying an actor removes its reminders.
132+
## Cancel a reminder
133+
134+
`unschedule` cancels one reminder: by operation, by operation and key, or by
135+
the handle that the operation call on the `schedule` result returns.
136+
`unschedule_all` cancels every key of one operation.
137+
138+
```ruby
139+
def confirm(buyer:)
140+
self.holds = holds.except(buyer)
141+
unschedule(:expire, key: buyer)
142+
end
143+
144+
def close_sale
145+
unschedule_all(:expire)
146+
end
147+
```
148+
149+
A cancellation is staged like a schedule. It commits with the state change of
150+
the same turn, and a turn that raises cancels nothing. A cancellation of a
151+
reminder that does not exist is not an error. An unknown operation raises
152+
`UnknownMessage`. A cancellation cannot recall an occurrence that the scheduler
153+
already turned into a message.
154+
155+
A reminder also stops when its handler does not re-arm it, and the destruction
156+
of an actor removes its reminders.
157+
158+
`reminder` returns the status of one reminder, or `nil`. `reminders` lists
159+
every key of one operation. Both include the changes that the current turn
160+
staged.
134161

135162
Self-scheduling actors should also have a low-frequency application reconciler.
136163
It may read `SolidObjects::Instance.states_for`, `.without_pending_work`, and
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# frozen_string_literal: true
2+
3+
require "test_helper"
4+
5+
class ReminderDocumentsTest < ActiveSupport::TestCase
6+
ROOT = File.expand_path("../..", __dir__)
7+
REMINDER_METHODS = %i[schedule unschedule unschedule_all reminder reminders].freeze
8+
9+
def guide(path)
10+
File.read(File.join(ROOT, path))
11+
end
12+
13+
test "the reminder guide names each public reminder method of an actor" do
14+
REMINDER_METHODS.each do |method|
15+
assert SolidObjects::Actor.public_method_defined?(method), "SolidObjects::Actor has no public #{method}"
16+
assert_includes guide("docs/reminders.md"), "`#{method}", "docs/reminders.md does not name #{method}"
17+
end
18+
end
19+
20+
test "the reminder guide does not deny a method that an actor has" do
21+
refute_match(/has no `unschedule`/, guide("docs/reminders.md"))
22+
end
23+
24+
test "the agent guide shows how to cancel a reminder" do
25+
assert_includes guide("docs/agents.md"), "unschedule(:expire, key: buyer)"
26+
end
27+
end

0 commit comments

Comments
 (0)