FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

docs: lead with the case a lock cannot cover by cardmagic · Pull Request #54 · cardmagic/solid-objects-ruby · GitHub

docs: lead with the case a lock cannot cover - #54

Open
cardmagic wants to merge 5 commits into
mainfrom
docs/lead-with-ticket-sale
Open

docs: lead with the case a lock cannot cover#54
cardmagic wants to merge 5 commits into
mainfrom
docs/lead-with-ticket-sale

Conversation

Copy link
Copy Markdown
Owner

Framing fix to the first screen and the fit sections. No API documentation was
rewritten, and no runtime code changed.

Why

The gem was posted to r/rails: 3.8K views, two comments, both negative. One
said the lead example was "the worst example you can come up for a gem, because
one can do that in one line of SQL." That was correct. The README opened with a
counter that incremented an integer, so the first thing a skeptical reader saw
was the argument against installing anything.

The homepage was rewritten in response. This ports what worked there.

The lead example

A counter needs one thing from one number, and SQL already does it. The ticket
sale needs three:

  • it must never go below zero;
  • it must give the seat back if the buyer does not pay within ten minutes; and
  • it must show the current count to everyone watching the page.

The first is one line of SQL. The second is an expires_at column plus a cron
job. The third is a broadcast on every path that changes the number. The
combination is the argument, and the example now shows all three in one class:
the guard, schedule(at: 10.minutes.from_now, key: buyer), and
observable :remaining.

Answering the objection where it is asked

## Why not just use with_lock? now sits in the first screen. It concedes
first, in the first sentence, because a concession is what buys the right to be
believed in the paragraph after it. Then it argues scope rather than discipline:

A lock is scoped to one transaction, on one connection, in one process. The
ticket sale above leaves that scope on one line: the hold expires in ten
minutes, and no transaction stays open for ten minutes.

Any column named expires_at, scheduled_at, or next_run_at is evidence
that the critical section already outlived the lock that was supposed to cover
it. What follows such a column is a sweeper that looks for due rows, and then
a race between that sweeper and the next writer of the same row.

Arguing "you might forget the lock" would lose that exchange, because the reader
answers "so remember" and they are right.

Also

  • ## Is it worth installing here? moves into the first screen, and says who
    should not use this, specifically. A section at line 1245 of a 1362-line file
    does not exist.
  • The Comparisons table gains the row people actually reach for: with_lock or
    SELECT ... FOR UPDATE.
  • A "Not a replacement for SQL transactions" callout is ported from the sibling
    README, which was ahead of this one on honesty, and given the reason the limit
    bites rather than only asserting it.
  • The Reactive ERB section concedes broadcast_replace_to before explaining the
    two gaps: two pushes can arrive out of order, and a push is not part of the
    save.

One bug found while verifying

The homepage example stores Time.current in actor state. Serialization
normalizes to JSON native types and raises InvalidPayload for anything else,
so that reserve never commits; the caller waits out its timeout instead:

actor invocation timed out after 5 seconds for TicketSale("event-42").reserve
message_id=1 sequence=1 status=ready waiting_on=not_yet_available

The README version stores Time.now.utc.to_i. solidobjects.dev still has the
broken line and should be corrected separately.

Validation

Every example in the changed sections was executed against this branch, not
read for plausibility:

reserve alice: true
reserve alice again: false
reserve bob: true
remaining: 98
reminders armed: 2
after expire, remaining: 99
holds: ["bob"]

bundle exec rake (test, standard, rubocop, rbs, steep, security): pass.

Two comments on r/rails, both negative, and the first one was right: a
counter is the worst possible lead example, because one line of SQL does
it. The README opened with exactly that counter, so the first thing a
skeptical reader saw was the argument against installing anything.

The lead is now the ticket sale from the homepage: 100 seats, a hold, a
ten-minute expiry that frees the seat, and a live count. It is the
smallest example that needs three things from one number, and the three
things are the actual argument. A counter needs one.

The objection now gets answered where it is asked instead of at line
1245 of a 1362-line file. A new section concedes with_lock first, then
makes the case on scope rather than discipline: a lock is scoped to one
transaction, on one connection, in one process, and any expires_at or
scheduled_at column is evidence the critical section already outlived
it. What follows that column is a sweeper, and then a race between the
sweeper and the next writer. Arguing that someone might forget the lock
would lose, because the reader answers "so remember".

A short worth-it and not-worth-it section moves into the first screen,
the Comparisons table gains the row people actually reach for, and the
reactive section concedes broadcast_replace_to before explaining where
push-after-write drops an update.

The homepage copy stored Time.current in actor state. State normalizes
to JSON native types only, so that reserve never commits. The README
version stores an integer.
The reactive paragraph claimed the fragment is re-rendered once per
change from the saving turn. The saving turn inserts the broadcast row in
the same transaction as the state change, which is the part that answers
push-after-write. Delivery is a separate worker that retries, so "once
per change" promised something the runtime does not.

The claim now matches docs/correctness.md: the row retries until it
succeeds, Cable ignores an older (instance_id, state_revision) pair after
a newer one, and the guarantee is that a viewer cannot end up on an older
number.

Greptile raised this on the JavaScript counterpart. The same sentence was
wrong here.
The README had become a manual pasted into the front door: 1466 lines,
with sections that docs/ already covers in more depth than the README
did. Length is not thoroughness. A reader who bounces at screen three
never reaches the reference material that justified the length.

Removed what docs/ owns outright. The dashboard section restated
docs/dashboard.md, and Reactive ERB restated docs/realtime.md for 167
lines, so both are now the argument, the smallest working example, and a
link. The Cloudflare capability subsections collapse to one paragraph
each, keeping the mapping table and the Solid Queue ordering comparison,
which live nowhere else.

Removed what the README said twice. "When to use it" and the new "Is it
worth installing here?" were the same section 1100 lines apart; the
early one now carries the domain list. Invoking an object explained
per-adapter lock deadlines that docs/correctness.md documents properly.

Removed what was no longer true. The Status list claimed "Implemented
and tested in 0.4" at version 0.14.1 and said the supervisor does not
replace a crashed role, which docs/roadmap.md lists as implemented.

Kept every fact that has no other home: the keyed-alarm section, the
configuration defaults table, the CLI worker-count flags, connects_to
for a separate actor database, the keyword-disambiguation rule,
InvalidRejectionCode, and the turbo-rails dependency.
The README was still twice the size of its JavaScript sibling at 1175
lines, and the difference was not extra capability. It was reference
material sitting on the front door: every reminder rule, every
configuration default, every CLI flag, and two registration examples,
all of which a reader passes on the way to deciding whether to install
anything.

Moved rather than deleted, because most of it had no other home.
docs/reminders.md is new and holds the keyed-alarm rules, the composed
name limit, the one-alarm-for-a-whole-queue drain, and reconciliation
for self-scheduling actors; docs/architecture.md now links there instead
of into a README heading. docs/operations.md gains the configuration
defaults table, the worker-count flags, the upgrade sequence, the
RuboCop exclusion, the Sorbet note, and the extension-component
contract. docs/architecture.md gains register_effect and
register_commit_action with their signatures.

Deleted only what was said twice. The table of contents duplicated the
headings GitHub already renders. A second example actor demonstrated
what the ticket sale demonstrates. Actor identity was three paragraphs
that fit in one, inside the section that defines an actor.

The README is now 528 lines and keeps what only it can say: the ticket
sale, the lock argument, who should not install this, the Durable
Objects mapping, and one worked example per capability with a link to
the guide behind it.
The lead actor declared observable :remaining and the ERB beside it
rendered that value in a span while a component observed :holds. Neither
worked. A scalar span raises ArgumentError unless the observable is
declared broadcast: :value, and a component dependency must itself be a
declared observable or ComponentRegistration raises
UnknownComponentDependency.

remaining now carries broadcast: :value and holds is declared, which is
also the clearer teaching example: one observable opts into sending its
value to every authorized subscriber, the other only invalidates, which
is what the paragraph under it describes.

Verified against this branch rather than read: broadcasts_observable_value?
is true for remaining, holds resolves as a dependency, and reserve,
duplicate reserve, and expire behave as the text claims.
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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant


Back | FazBrowse Home | New Git URL