Conversation
Match JavaScript telemetry and actor diagnostics using native Active Support notifications and Active Record scopes. Isolate subscriber failures while preserving application exceptions, and keep private error text out of default logs. Refs cardmagic/solid-objects-js#42
|
Keep optional database measurements from failing durable delivery. Cover broadcast age and report message duration only on terminal outcomes so Ruby and JavaScript emit matching samples.
|
@greptileai Please review the current head, 7149e9c. The existing summary still references the initial commit.
All current CI jobs pass. Please reassess the full diff and update the summary for this head. |
Reauthorize message reads and retain bounded JSON results for background operations so Ruby and JavaScript provide the same outcome contract. Normalize polling telemetry units and preserve the matching transmit validation and ordering guarantees in shared fixtures and documentation.
|
@greptileai Please review current head 01b2981 and update the summary for this commit. The parity fixes reauthorize every message read, retain bounded JSON results for background operations, share terminal result errors with synchronous calls, normalize polling telemetry, and update compatibility tests and documentation. The full Ruby suite and focused PostgreSQL checks pass; reversal checks reproduce seven failures with the old implementations. Please review the full diff, including these compatibility changes. |
Reject actor state changes and durable intents from queries and observable projections before they can commit. Fail these violations without retrying, matching the JavaScript runtime. Pin reserved JSON names with shared fixtures and correct reminder limits and dead-transmit recovery documentation.
|
@greptileai Please review current head 4530819. The latest change rejects state mutation and all staged durable work in queries and observable projections, including individual snapshot projections, with terminal QueryMutatedState. Pure projections preserve work staged by normal operations. Reversal reproduces 13 failures; all 830 Ruby tests, lint, RBS, Steep, Brakeman, and focused PostgreSQL checks pass. Shared JSON fixtures and corrected reminder/retry documentation complete the audit follow-up. Please review the full diff and update the current-head summary. |
Compare complete staged work around projections so draining one intent and staging another cannot bypass the read-only contract. Cover effect and commit-action replacement with terminal rollback regressions.
|
@greptileai Please review the latest commit d9667bd. The observable replacement finding is fixed by comparing deep snapshots of all staged work, with effect and commit-action reversal regressions. The equivalent JS guard is fixed too. All 832 Ruby tests and static/security checks pass. |
Guard and isolate payload projections, honor configured byte limits, and preserve generated defaults within each committed snapshot. Normalize timeout telemetry to the shared JavaScript contract. Let SQLite lock holders run during background busy waits on older Rails versions, reinstalling the yielding handler on each attempt. Cover the behavior with shared fixtures, real adapter regressions, and reversal checks.
A parity audit found portable events with different names and fields in Ruby and JavaScript. Only the timeout event had a shared fixture, so no test found the difference. compatibility/telemetry-events.json now holds the attribute allowlist and the exact keys of each core SQL event. Both test suites read the same file. - Rename payload_broadcast_failed to payload_broadcast.failed, the dotted form that all other events use. - Send operation, deliveryMode, retryable, outcome, commitAction, and the outbox identity, as JavaScript already does. - Log exporter and observer failures. Ruby discarded them before. - Require an observer block and accept at most 1,000 observers. - Pin reserved JSON keys through actor arguments, state, and results.
Two behavior changes in this branch had no release note. The activation.started event now fires before the activate hook, and activation.completed takes its earlier meaning. Active Support payloads and the worker error log no longer carry error_message, because exception text can contain actor state. The shared observability guide had no Ruby setup sample and named UnsupportedCapability as if Ruby had it. Add the sample and mark the Durable Objects behavior as JavaScript only.
The telemetry type contract needs the same packaged-gem Steep check as the effect payload contract. Move the helpers into one module so both tests use them.
Observer blocks, diagnostics, telemetry helpers, and message results used untyped, so Steep accepted a consumer that read the wrong field. sig/public/telemetry.rbs now publishes portable_event, metric_sample, event_observer, diagnostic_summary, and actor_diagnostics. A json_value type covers message results and actor state. A strict packaged type test checks a consumer and five invalid versions of it. With the old untyped observer block, the invalid observer consumer type-checked. authorization_context stays untyped, as on main, because it holds the application's own subject.
One 0.25 s deadline covered two steps: the actor start and the lock wait under test. When a slow CI runner took more than 0.25 s to start the actor, the call timed out before the actor ran, and the test waited 2 s for it at line 422. This failed the Rails 7.1 compatibility job in run 36877758705. Give the call 1 s, and set the outer limits to 3 s and 2 s. A 0.3 s stall before the actor claim reproduced the CI error with the old budget; with the new budget, 0.3 s and 0.6 s stalls pass. The limits stay below the 5 s SQLite busy timeout: when the deadline never expires, the test fails at line 443 on Rails 7.1 and 8.1.
Summary
Add portable observability for cardmagic/solid-objects-js#42 and align message behavior with JavaScript #59. Both runtimes expose versioned JSON events, metric samples, authorized local observers, and bounded actor diagnostics without exposing private payloads.
max_payload_bytes, including UTF-8 boundaries and limits above 1 MB.waitingOn,activationOwnerId, and stringactivationGenerationfields. Normalize portable wait reasons to the shared camelCase values while preserving native Ruby diagnostics.QueryMutatedState, matching JS; normal operation intents survive pure projections. Guards compare complete staged work, so replacing an intent without changing the count also fails.compatibility/telemetry-events.jsonwith JS. It holds the attribute allowlist and the exact keys of each core SQL event, and both test suites check their events against it. Message events now sendoperationanddeliveryMode,message.failedsendsretryableandoutcome, and commit action events send the message fields andcommitAction.reminder.enqueued,outbox.age, andsync.enqueue_timeoutsend the same keys as JS, and polling intervals are integers.solid_objects.payload_broadcast_failedtosolid_objects.payload_broadcast.failed, the dotted form that every other event uses.solid_objects.instrumentation.failedwhen an exporter or observer raises; Ruby discarded these errors before. Observers need a block, and a process accepts at most 1,000 observers, as in JS.Examples
These snippets extend an application's registered
ShoppingCartactor.operatoris the current authenticated caller; the application's message and administration policies must authorize it.Send portable events and metric samples to your logger
Events include
schemaVersion, actor/message correlation, safe attributes, and metric samples. Ruby and JS emit the same JSON fields, including polling intervals in milliseconds. Arguments, state, results, and exception text are excluded; exporter failures do not fail actor work. Existing Active Support notifications remain available.Watch one actor and inspect its queues
Administration policies must allow
:observeand:inspectonactor_diagnostics. Each category exposessampled,truncated, andoldestAgeMilliseconds. Usecart.observe(authorization_context:) { |event| ... }for all of this actor's events. Observers are local to the current process.Recover a background result after losing the original handle
Assume
checkout(order_id:)returns{ "order_id" => order_id }. Enqueue it with a stable key:After a worker processes it, a later request can recover the result:
A completed result is
{ "order_id" => 42 }. Background results are now retained, validated as JSON, and bounded bymax_result_bytes. Every read checks current authorization against the original operation and arguments.resultraisesSolidObjects::RejectedorSolidObjects::MessageFailedfor terminal failures;outcomeexposes them as data.Keep two transmits in staging order through retries
Inside an actor operation, stage two calls to its server twin's declared
appendoperation:With the transmit handler registered, the receiver applies
1before2, even if delivery of1initially fails. Returningnilexplicitly avoids retaining an incidental result. This PR pins Ruby's existing order with a retry regression test; the paired JS PR fixes its ordering to match. Both ingest paths reject explicit null arguments and deduplicate repeated envelopes.Diagnose why a synchronous call timed out
Filter
solid_objects.sync.timeoutevents to see whether an activation, an earliermessage, a paused actor, or database contention prevented progress. The Ruby and
JS SQL runtimes now retain the same fields and reason values. The Durable Objects
host sends no
sync.timeoutevent; itscalltimeouts reportwaitingOn: "unknown".For example, an event can include:
{ "name": "solid_objects.sync.timeout", "attributes": { "waitingOn": "activationHeld", "activationOwnerId": "worker-1", "activationGeneration": "7" } }Unknown activation fields are null. These are correlation fields, not metric labels.
Compatibility
Queries, observables, and personalized payloads must be pure: they cannot change actor state or stage effects, recovery checks, commit actions, reminders, or outbound messages. Query and observable violations fail without retries; a payload violation is confined to that payload and instrumented. Projection guards compare state and staged work before and after evaluation. Payloads also prevent application database writes and allocate an isolated actor for each evaluation.
Message-reference reads now accept
authorization_context:and reevaluate policy on every read. Callers using protected actors must supply their current context.All operation return values, including background calls, must serialize as JSON and fit
max_result_bytes. Returnnilwhen an operation needs no result. Previously discarded background results cannot be recovered retroactively. No Ruby database migration is required.Observers are local to the current process. Diagnostic samples are bounded observations; they do not provide a transactional snapshot across the fleet.
Active Support subscribers to
solid_objects.payload_broadcast_failedmust subscribe tosolid_objects.payload_broadcast.failed. The Active Support payload keepspayload_name; the portable event names itpayload.observeandonraiseArgumentErrorwithout a block or after 1,000 observers in one process.solid_objects.activation.startednow fires before theactivatehook. Before, it fired after a successful hook. The newactivation.completedevent takes that meaning, andactivation.failedreports a failed hook. JavaScript changes the same events in the paired PR. Move a subscriber that readsactivation.startedas a finished activation toactivation.completed.Active Support payloads no longer carry
error_messageforcommit_action.failed,activation.deactivation_failed,supervisor.monitor_failed,supervisor.retention_failed,supervisor.redrive_failed, andwake_up.failed. Thesolid_objects.worker.errorlog entry also omits it. Each keepserror_class. Exception text can contain actor state; JavaScript already reports only the error name.Validation
Timeout::Erroratsynchronous_invocation_test.rb:422, because one 0.25 s deadline covered the actor start and the lock wait. A 0.3 s stall before the actor claim reproduces that error with the old budget. With a 1 s budget, 0.3 s and 0.6 s stalls pass. WhenSyncDeadline.expired?never returns true, the changed test fails at line 443 on Rails 7.1.6 and 8.1.3.1. The whole file passed three random orders on Rails 7.1.6;bundle exec rakeran 864 tests with 0 failures/errors and 28 skips.activation.started attributesexpectedownerId;ArgumentError expected but nothing was raisedfor a blockless observer and for observer 1,001; the polling JSON showed100.0where the contract needs100;Expected [] to include {event: "solid_objects.instrumentation.failed", ...}for a failed exporter. All pass after the fix.bundle exec rakeran 863 tests and 3,265 assertions with 0 failures/errors and 28 existing skips. Standard, RuboCop, RBS validation, Steep, and Brakeman passed.SolidObjects::QueryMutatedState expected but nothing was raised, timeout metadata becoming{}, a fixed 1 MB cap rejecting an explicitly larger limit, andSQLite3::BusyException: database is locked. Restoring the implementation passes.MessageFailed expected but nothing was raisedfor both effect and commit-action replacement. Restoring the fix passes all 16 purity tests.