Skip to content

CRuby Db: rate-limit checkpoint warns + flock takeover test - #59

Closed
thomasklemm wants to merge 248 commits into
mainfrom
cursor/wal-checkpoint-harden-6c87
Closed

thomasklemm wants to merge 248 commits into
mainfrom
cursor/wal-checkpoint-harden-6c87

Conversation

@thomasklemm

Copy link
Copy Markdown
Owner

Advances the post-rubys#548 hardening from the Campfire #319 vs Roundhouse review — keeps RH’s opt-in design (no global wal_autocheckpoint: 0).

Summary

  1. Rate-limited checkpoint failure visibility in runtime/spinel/db_cruby.rb: warn_checkpoint_failure emits at most once per 30s. The loop keeps its 250 ms cadence (no Campfire-style backoff / log storm).
  2. Flock takeover E2E in tests/db_sqlite_concurrency.rs: a foreign flock holder is killed; the survivor’s background checkpointer acquires the lock and copies the WAL while wal_autocheckpoint=0.
  3. Honest Spinel comment in runtime/spinel/db.rb: measured lane is WORKERS=1; do not add a second flock home for --workers N.

Intentionally not ported from Campfire rubys#319: global yml pragma, WEB_CONCURRENCY=auto gating, Resque hooks, Spinel flock, per-tick warn storms.

Test plan

  • cargo test --test db_sqlite_concurrency — 12 passed, 1 ignored (Spinel)
  • CI on this PR

Refs

Open in Web Open in Cursor 

dchuk and others added 30 commits October 5, 2026 10:30
Infer unsigned byte integers and share one no-block bridge classifier across
targets. Preserve native Ruby/Spinel block behavior and report unsupported
strict-target block forms explicitly. Add emitted execution regressions.

Fixes rubys#433.

Co-Authored-By: Codex <noreply@openai.com>
Use the shared byte-array classifier at the forwarded-proc guard and
exercise explicit nil blocks, refusal boundaries and generated RBS seeds.
Document the target entry points and regression intent.

Co-Authored-By: Codex <noreply@openai.com>
Return an ingest error instead of silently omitting external engine mounts.
Keep survey recovery explicit and preserve the runtime's built-in
ActiveStorage routes. Cover strict and survey CLI behavior with a generic
path-sourced engine for Ruby and Spinel, and correct the route documentation.

Co-Authored-By: Codex <noreply@openai.com>
Preserve supported runtime cable mounts and recover sibling routes with attributed error diagnostics. Keep strict refusal, allow explicit unsupported recovery, and aggregate check errors across route and application sources.

Co-Authored-By: Codex <noreply@openai.com>
Declare the emitted Rack test prerequisite in unit CI and contributor setup. Keep the real request assertions unchanged.

Co-Authored-By: Codex <noreply@openai.com>
Seed method bodies and receiver dispatch with qualified RBS identities.
Keep only unique short aliases and tighten the measured residual ceiling.

Co-Authored-By: Codex <noreply@openai.com>
Fixes rubys#430.

Co-Authored-By: Codex <noreply@openai.com>
Fixes rubys#429.

Co-Authored-By: Codex <noreply@openai.com>
Address the additional-target regressions found on PR rubys#439.

Co-Authored-By: Codex <noreply@openai.com>
Retain Rack for the required mount dispatch test alongside upstream ruby-vips and libvips42 prerequisites. Preserve both branches' source changes.

Co-Authored-By: Codex <noreply@openai.com>
Fix the scoped Swift request error boundary and shared Router portability on PR rubys#439.

Co-Authored-By: Codex <noreply@openai.com>
Preserve the canonical identity fix and strict 45-site probe ceiling.
Retain upstream's measured legacy-probe history without treating it as
the canonical probe's ceiling.

Co-Authored-By: Codex <noreply@openai.com>
Preserve zero, maximum-byte and bounds-rejection expectations while avoiding Swift infix try syntax in the generated framework tests.

Co-Authored-By: Codex <noreply@openai.com>
Preserve upstream changes and both probe explanations without changing the casting implementation or its assertions.

Co-Authored-By: Codex <noreply@openai.com>
Forward flattened int_params to the production and test Route constructors.
Cover multiple constrained keys and unconstrained controls with generic
emission and native Swift request regressions.

Co-Authored-By: Codex <noreply@openai.com>
`redirect(path: "/x")` keeps the request query string. The synthesized
action built that from `ActionController::Current.request` and a
`split("#", 2)` whose `parts[0]` is `String?`, and the analyzer rejected
both. Every app with a path-option redirect got errors with no file or
line: 3 on real-blog with such a route, and 10 on Jumpstart Pro for its
three Devise-era `/users/*` redirects. Nothing ran the emitted action,
so no test caught it.

The action now reads the query off its own `request`, as the
block-redirect actions already do. Only `path:` keeps the query, and
its location is a string literal, so the rest is decided at compile
time: the `&` or `?` separator, and where the fragment goes. A `%{name}`
filled from a path segment cannot bring a `?` or `#` of its own. The
emitted action is one conditional over two literals, for example
`q == "" ? "/a#top" : "/a?" + q + "#top"`. The behavior is unchanged:
- an empty query leaves the location alone
- a path that already has a `?` is joined with `&`
- the query goes ahead of a fragment

That behavior is a deliberate divergence from Rails, now written down
in the code. Measured under Rails 8.1.4 and main, OptionRedirect only
appends `"?" + query`. It gives `/a?b=1?x=1`, where `b` reads as
`1?x=1`, and `/a#top?x=1`, where the query is inside the fragment and
never reaches the server. Rails also re-encodes the query (keys
sorted, a space as `+`). This passes it through as received.

Pinned by `tests/emit_and_run.rs::a_path_option_redirect_keeps_the_request_query`.
It failed with the three errors before the change and now runs each
case through a request. The lowering tests in routes_redirect_lowering.rs
follow the new shape.

Co-Authored-By: Claude <noreply@anthropic.com>
Review on rubys#456 asked whether a `%{id}` holding a `#` can move the kept
query into a fragment. In Rails it would, if Rails did not re-escape:
it decodes `/old/a%23top` to `params[:id] == "a#top"` and then
path-escapes it back. The emitted router does neither step. A capture
keeps the request's own percent-encoding, and a request path cannot
carry a literal `#` or `?` in a segment, so the segment passes through
as sent. That is the location Rails builds, apart from Rails'
normalizing (`%7e` becomes `~`). Escaping it here would double-encode
it (`%2523`), which I tried and measured.

The comments that claimed a capture "cannot bring a `?` or `#`" now say
why that holds. The emitted test adds the reviewer's case and a
positional redirect with an encoded space and `?`. Both expectations
are Rails 8.1.4's answers for the same requests.

Co-Authored-By: Claude <noreply@anthropic.com>
The shared router handed every capture over still percent-encoded.
`/articles/a%20b` gave `params[:id] == "a%20b"`, where Rails' Journey
router (`CGI.unescapeURIComponent`, 8.1.4 and main) gives `"a b"`. That
held on every target, for `:name` segments, `*glob` captures and
prefixed segments (`~:username`) alike.

`Router.decode_segment` now decodes each capture once:
- `%20` is a space, `%2F` a slash inside the segment, and `%25` a percent.
- A `+` stays a `+`. Form decoding belongs to the query, not the path.
- A `%` without two hex digits after it stays as written, as in Rails.

It is pure shared Ruby in the shapes the router's other helpers
already lower everywhere: one `while` per method, `==` fan-outs, and no
regex. So it covers printable ASCII only (`%20`..`%7E`), through a
lookup string. A non-ASCII sequence such as `%C3%A9`, or a control
byte, stays encoded. That is no worse than before, and the code says so.

Three servers decoded the path before routing, which would now decode
it twice. They hand the router the raw path instead:
- Go: `URL.EscapedPath()`
- Python: aiohttp `rel_url.raw_path`
- C#: the request feature's `RawTarget`, minus the query

The TypeScript, Crystal, Rust, Kotlin, Swift (Hummingbird's `uri.path`
is a substring of the request line), Elixir, Ruby and Spinel servers
already pass the raw path.

A routing redirect's `%{name}` is filled from the decoded capture, so
it is now path-escaped again as Rails does
(`Journey::Router::Utils.escape_path`), through `Router.escape_path`,
the decoder's inverse. Otherwise a decoded `#` or `?` would end the
path. It leaves alone a `%XX` that `decode_segment` left encoded, so a
non-ASCII name answers `/~jos%C3%A9` as Rails does, not `%25C3%25A9`.
The analyzer exposes that one router method to app code.

Pinned by:
- `tests/emit_and_run.rs::a_path_parameter_is_percent_decoded`: six
  requests, each expecting what Rails 8.1.4 answers.
- The redirect test's encoded-capture cases: `a%23top`, `a%20b%3Fc`,
  `jos%C3%A9`, `100%25`, `a%2Fb%7e` and `x%0Ay`, with Rails' answers.
- Two new runtime router tests for `decode_segment` and `escape_path`.
  They pass under CRuby and under the TypeScript framework suite. In
  the Rust framework suite they compile, and that suite fails on
  upstream already.

Claude-Session: https://claude.ai/code/session_01AxRosKWn6BKiBwzju7wjrU
…orm target in C#

`go_smoke::router_v2_shape` pinned the old `params[name] = ap`. The
router now assigns `decode_segment(seg)`, so the test pins that.

The C# server read the raw request target as the path, but a request
sent in absolute form (`GET http://host/articles`, as to a proxy)
carries a scheme and host there. It now takes `Uri.AbsolutePath`,
which stays escaped, and still falls back to `Request.Path` when there
is no raw target.

Claude-Session: https://claude.ai/code/session_01AxRosKWn6BKiBwzju7wjrU
Preserve both byte contract tests and upstream view rendering regressions.
Validate the complete default suite on the merged source and retain the
independently reproduced upstream native boot limitation in PR evidence.

Co-Authored-By: Codex <noreply@openai.com>
Preserve canonical RBS seeds and finder-specific zero-residual assertions.
Record independently measured upstream residual growth from 45 to 57,
with identical expression paths on upstream and the finder candidate.
Ledger the independently measured unseeded probe growth from 497 to 505
after upstream membership/cardinality changes; retain fully typed RBS gates.

Co-Authored-By: Codex <noreply@openai.com>
Retain both byte regressions and upstream view-rendering execution tests.
Keep the route implementation and contract expectations unchanged while
validating the resulting source against current upstream.

Co-Authored-By: Codex <noreply@openai.com>
Validate Float bounds before truncation instead of parsing scientific
notation as a decimal String prefix. Align finder and parser input types,
retain String semantics, and exercise lowered calls under Ruby and Spinel.
Record the measured seven-site unseeded inference delta while preserving
the separate fully typed RBS finder gates.

Co-Authored-By: Codex <noreply@openai.com>
Return a transient statement while an identical cached shape is owned.
Reset and clear each released handle once, track replay promotions, and
drain abandoned siblings at the lease boundary, including failed requests.

Evict statements after failed cleanup, retain resources whose close failed,
and quarantine their connections. Track missing pool capacity if replacement
fails; wake waiters to retry or raise the opener error. An exhausted checkout
can restore the slot when the opener recovers. Healthy checkout is unchanged.
Preserve the original request exception when cleanup also fails.

Extend the shared CRuby/compiled Spinel ownership gate and inject reset,
clear, close, replacement and shutdown failures in the real drivers. Run
the JDBC cleanup and unbound ownership probes in the existing JRuby CI job,
and route their paths through the planner.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Concern that declares `class_attribute :x, default: D` in `included do`
and writes it from `class_methods` (`self.x += [...]`) left every macro
call in the includer unrecognized, and its effect dropped. The macro's
arguments may be computed at run time, so it is not evaluated here:
the emitted class makes the same call when it loads.

Each direct includer gets the Concern's class methods as its own, the
attribute as a class ivar seeded with its default at the `include`, and
a reader. `self.x = v` becomes `@x = v`; `self.x op= v` becomes
`@x = x op v`, read through the reader. A subclass that never writes
reads its parent's value, as class_attribute inherits; a write replaces
it on that class only. All or nothing per carrier: another `included`
statement or a source `@x` leaves the carrier unexpanded.

The Ruby emitter now writes class-body initializers after the methods
they may call.

Spinel refuses `==` on a local holding such a reader's result
(matz/spinel#7602); the native contract reads off the class directly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The class methods `ingest::class_attribute` copies onto a controller were
typed with every parameter unresolved: nothing observed their calls.

- A class-body macro call is a call site, and one written in a subclass
  reaches the controller that defines the method.
- `**{k: v}.merge(h)`, which ingest writes for `f(k: v, **h)`, places
  the literal's keys and gives each other keyword `h`'s value type.
  A bare `**h` stays unplaced, as before.
- `Kernel#Array(x)` takes its element from `x` instead of RBS untyped.
- `[] + b` (and `|`, `concat`) onto an unknown element takes `b`'s.
- The attribute's type is what its methods store; on the class that
  includes the Concern it is never unset, the default being stored at
  the `include`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- `Kernel#Array` of a tuple joins the tuple's element types instead of
  wrapping the tuple.
- An app method named `Array` keeps its answer even when that answer is
  untyped; only the Kernel fallback is refined.
- `**{k: v}.merge(h)`: `h` merges over the literal, so a named key's type
  joins the splat's value type.
- Each copied class method is keyed to the class_attribute it writes,
  not the first one declared; a method writing two refuses the carrier.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…a relation

- A copied class_attribute method's signature declared every parameter
  with the attribute's type; it now declares what the call sites seeded
  (a finite writer still takes the slot's).
- A merged splat's literal keys join its value type where the call site
  is recorded, so every placement path sees it.
- `Kernel#Array` of a relation is its records, of a range its elements;
  a record is left unknown, like a Hash.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review follow-ups.

- A subclass that set its attribute to nil read its parent's value: the
  reader fell back on nil. Each write now also sets `@name__written`, and
  a subclass reader falls back only when that flag is unset, so nil is
  the subclass's value as in Rails. Spinel has no
  `instance_variable_defined?` on a class, hence the flag.
- A copied class method's parameter defaults are typed before its
  parameters are seeded, so `only: nil` declares the nil it takes when
  the argument is absent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Treat native SQLite bind and step return codes as errors rather than success
or EOF. Reset failed native cursors and drop their query-cache captures, but
retain checkout ownership until explicit finalize or lease cleanup. An old
ensure-finalizer must never reset another reader using the same raw pointer.

After CRuby/JDBC cleanup, re-raise the original driver exception with its
class and identity intact, even when cleanup also fails. Keep JDBC failed
statements evicted. JDBC bind setters remain in the later JDBC commit.

Cover bad bind slots, real integer-overflow step errors, same-lease recovery,
and old finalizers on CRuby and compiled Spinel. Assert actual driver error
classes and identities on CRuby and JDBC, including cleanup failures.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
cursoragent and others added 28 commits October 7, 2026 15:49
Inherit a real controller-file span for AOT controller_name/path
literals (main rubys#552 left them synthetic and broke span preservation).
Mirror library survey-mode honesty: model expand_mattr_cattr uses
ingest_expr_strict so nil substitution cannot become a claimed seed.

Co-Authored-By: Cursor <cursoragent@cursor.com>
Co-authored-by: Thomas Klemm <github@tklemm.eu>
Attribute controller_name literals to controller source spans
Resolve controller_to_library conflict by taking main's rubys#573
controller_provenance_span (action-body preference + Option span).

Co-Authored-By: Cursor <cursoragent@cursor.com>

Co-authored-by: Thomas Klemm <github@tklemm.eu>
ingest_expr_strict still recurses through ingest_expr, so unsupported
nested subtrees can become synthetic nil under survey while the outer
call returns Ok. Mirror library and model claim paths: if the survey
collector grew during default ingest, leave the declaration unmodeled.

Co-Authored-By: Cursor <cursoragent@cursor.com>
Co-authored-by: Thomas Klemm <github@tklemm.eu>
Signed Locator verifies MessageVerifier envelopes under a purpose,
narrows with literal only:, and monomorphizes to locate_signed_<model>
the same way unsigned locate does — so strict targets never dispatch
through a class object.

Co-Authored-By: Cursor <cursoragent@cursor.com>

Co-authored-by: Thomas Klemm <github@tklemm.eu>
`for: purpose` is a default expression, not a local alias — bind
purpose from **opts[:for] instead so the generic Locator entry works.

Co-Authored-By: Cursor <cursoragent@cursor.com>

Co-authored-by: Thomas Klemm <github@tklemm.eu>
Mirror ActionText::RichText: push an ApplicationRecord model with
polymorphic belongs_to :record and schema columns (including optional
slug). Blob is reached via instance helpers (url/filename/content_type),
not a belongs_to — Blob is a runtime class.

Co-Authored-By: Cursor <cursoragent@cursor.com>

Co-authored-by: Thomas Klemm <github@tklemm.eu>
Reader always returns ActiveStorage::AttachedMany; attach_blob appends;
attachments answers Attachment rows. Array attachables lower to sequential
attach_blob/from_attachable. Preload with_attached_<name> → <name>_attachments.
Ratchet runtime untyped ceiling for the model-bridge Relation chain.

Co-Authored-By: Cursor <cursoragent@cursor.com>

Co-authored-by: Thomas Klemm <github@tklemm.eu>
When the hook reopens an app model with only literal include Module
calls, push those includes onto the model body (pending across file
order) so concern splice applies has_many_attached. Other reopens stay
surveyed.

Co-Authored-By: Cursor <cursoragent@cursor.com>

Co-authored-by: Thomas Klemm <github@tklemm.eu>
Overlay: has_many_attached array + attachments.last, Attachment finders,
locate_signed mint/find. Unit: synthesis, AttachedMany reader, on_load
carry. Update unclaimed-DSL and only_finder locate_signed rewrite tests.

Co-Authored-By: Cursor <cursoragent@cursor.com>

Co-authored-by: Thomas Klemm <github@tklemm.eu>
After save!, append a read of the model so inlined update! answers the
record, not Bool — clears Spinel AOT Page vs Base widening. Catalog
save! ReturnKind unchanged.

Co-Authored-By: Cursor <cursoragent@cursor.com>

Co-authored-by: Thomas Klemm <github@tklemm.eu>
Analyze registers blob/url/filename/content_type on the synthesized
ActiveStorage::Attachment model so redirect_to @attachment.url types.
Keep has_many_attached out of writable_field_set until a Many writer
exists (proxy attach only). Re-apply pending on_load includes after
Markdown/Attachment synthesis so synthesis-only targets still splice.

Co-Authored-By: Cursor <cursoragent@cursor.com>

Co-authored-by: Thomas Klemm <github@tklemm.eu>
Check errors 34→29; survey gaps 11→10 (on_load Attachment→Markdown
include carried). Cleared locate_signed, ActiveStorage::Attachment
constant, upload @record/@attachment, and Attachment#url.

Co-Authored-By: Cursor <cursoragent@cursor.com>

Co-authored-by: Thomas Klemm <github@tklemm.eu>
Rails only class_variable_sets a nil default when the @@ slot is not
already defined; unconditional @@attr = nil wiped inherited values.
Emit conditional defined?(@@attr) seeds for nil/absent defaults and pin
with a redeclaration runtime test.

Co-Authored-By: Cursor <cursoragent@cursor.com>

Co-authored-by: Thomas Klemm <github@tklemm.eu>
Second concern splice after Attachment/Markdown synthesis now targets
only models touched by apply_pending — a full re-splice doubled every
already-expanded concern body (Writebook where/order counts, Campfire
unique-constraint failures).

AttachedMany#attachments uses raw SQL + ManyAttachment (not Relation
over the synthesized Attachment MODEL), restoring Soft Bar B ceiling
304.

Co-Authored-By: Cursor <cursoragent@cursor.com>
Co-authored-by: Thomas Klemm <github@tklemm.eu>
The campfire page compares deployed shapes, route by route, as
once-campfire-rust's bench/run does: room page, messages?before=,
sidebar, search, avatar, a stylesheet, /up and a POST. The avatar
token and stylesheet digest are each lane's own, scraped from its room
page into {avatar}/{css} placeholders so cells line up across lanes.

The -1p, JVM-family and nocache lanes stay requestable by name but
leave the default set, and their notes print only when they ran.
bench-report takes the harness's endpoint order and lays the latency
table out a row per route when routes outnumber lanes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…5998

mattr/cattr values are shared across the class hierarchy
Every file under each app/assets directory and app/javascript lands
at static/assets/<path within it>, where Rack::Static serves it. The
emitted campfire linked 26 stylesheets plus its images, sounds and JS
modules this way and served none of them: /assets/_reset.css was a
404. A path the scaffold already builds (tailwind.css, application.css)
keeps its task.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tobi runs tobi/campfire-once-ruby-ractor as its Dockerfile does (ruby
--yjit bin/campfire, jemalloc, Rails' worker formula as Ractors in one
process) against the same seeded database, signing in with header-only
forgery protection like the rust and go lanes.

The emit directories are removed before each emit: the emitter never
deletes a file it stopped generating, and the box's spinel tree still
held a 09-25 index.rbs beside index_turbo_stream.rbs, which spinel's
--rbs now refuses as a duplicate declaration.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…estorage-abd0

Hill climb: Writebook as the forcing fixture for ActionText Markdown uploads / ActiveStorage attachments
GitHub can leave selected archive-results as abandoned on large ci:full
PR graphs even when every producer succeeded (run 37652830796). Treat
the packaging evidence job as REPORTING: still selected for completeness
and required by Pages assembly, but never a CI-summary/required failure.

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Preserve the instance receiver for literal controller_name and controller_path implementations. Pin the generated signatures and execute both Base and concrete-controller calls in the emitted Rust application.

Fixes rubys#575.

Co-Authored-By: Codex <noreply@openai.com>
The spinel binary serves static/assets, which only `make assets`
fills, and the harness ran `make build` alone: every stylesheet the
room page links was a 404 on that lane. bench-report prints the
per-route placeholders as :avatar_token, "a stylesheet" and
"POST <path>".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Make a module parameter's call-site join order-independent (rubys#209)
Keep app scopes and dynamic finders on the relation receiver (rubys#558)
Queue and other stdlib constants resolve again in emitted Ruby
Tep uses registered reasons for conflicts and validation errors
Serving checkpointers kept failing silently after wal_autocheckpoint=0.
Warn at most once per 30s so operators see stuck disk/permission paths
without a Campfire-style 250ms log storm. Pin leader-exit takeover: a
survivor's loop acquires the flock and copies the WAL. Honest Spinel
comment: WORKERS=1 is the measured lane; do not add a second flock home.

Co-Authored-By: Cursor <cursoragent@cursor.com>

Co-authored-by: Thomas Klemm <github@tklemm.eu>
@coderabbitai

coderabbitai Bot commented Oct 7, 2026

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

@thomasklemm thomasklemm closed this Oct 7, 2026
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.

9 participants