Skip to content

Commit 51692a6

Browse files
authored
Merge pull request #80 from cardmagic/feat/portable-observability
Align observability and message result contracts
2 parents 4e53160 + 78a5aca commit 51692a6

85 files changed

Lines changed: 2423 additions & 188 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎CHANGELOG.md‎

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

3+
## 0.17.0 - 2026-10-03
4+
5+
- Publish RBS types for portable events, metric samples, actor diagnostics, and
6+
event observers in `sig/public/telemetry.rbs`, and a `json_value` type for
7+
message results and actor state. Observer blocks, diagnostics, and results now
8+
type-check against these contracts instead of `untyped`.
9+
- **Breaking:** `solid_objects.activation.started` now fires before the actor's
10+
`activate` hook. Before, it fired after a successful hook. The new
11+
`solid_objects.activation.completed` event takes that meaning, and
12+
`solid_objects.activation.failed` reports a failed hook. JavaScript changes
13+
the same events. Move a subscriber that reads `activation.started` as a
14+
finished activation to `activation.completed`.
15+
- **Breaking:** Active Support payloads no longer carry `error_message`. This
16+
applies to `commit_action.failed`, `activation.deactivation_failed`,
17+
`supervisor.monitor_failed`, `supervisor.retention_failed`,
18+
`supervisor.redrive_failed`, and `wake_up.failed`. The
19+
`solid_objects.worker.error` log entry also omits it. Each keeps
20+
`error_class`. Exception text can contain actor state, so JavaScript already
21+
reports only the error name.
22+
23+
- **Breaking:** rename `solid_objects.payload_broadcast_failed` to
24+
`solid_objects.payload_broadcast.failed`, the dotted form that every other
25+
event uses. Update Active Support subscribers to the new name. The portable
26+
event names the payload `payload`.
27+
- Match portable event attributes to JavaScript through the shared
28+
`compatibility/telemetry-events.json` contract. Message events carry
29+
`operation` and `deliveryMode`, `message.failed` carries `retryable` and
30+
`outcome`, commit action events carry the message fields and `commitAction`,
31+
and `reminder.enqueued` carries `operation`. `outbox.age` carries the effect or
32+
broadcast identity, `sync.enqueue_timeout` carries `timeoutMilliseconds`, and
33+
polling intervals are integers. `realtime.connected` carries only actor fields.
34+
- Log `solid_objects.instrumentation.failed` when an exporter or observer raises.
35+
Observers require a block, a process accepts at most 1,000 observers, and
36+
`SolidObjects.reset!` removes them. Pin reserved JSON keys through actor
37+
arguments, state, and retained results.
38+
39+
- Guard personalized payload projections against state changes, staged work,
40+
and application database writes. Each payload gets an isolated actor from
41+
the committed snapshot and honors `max_payload_bytes`, matching JavaScript.
42+
- Preserve timeout wait reasons, activation owner IDs, and activation generations
43+
in portable telemetry using the shared camelCase fields and reason values.
44+
- Use a yielding SQLite busy handler for background transactions so concurrent
45+
writers can finish on Rails 7.1 and 7.2. Preserve configured wait limits and
46+
synchronous deadlines; cover contention with a coordinated lock regression.
47+
48+
- **Breaking:** reject query and observable state mutation and staged durable
49+
work with terminal `QueryMutatedState` errors. Cover individual snapshot
50+
projections and preserve ordinary operations' already-staged work while
51+
reading projections, including replacements that leave the intent count
52+
unchanged.
53+
- Pin reserved JSON property names with shared Ruby/JS fixtures. Document the
54+
reminder-name limit difference and the authorized dead-transmit retry API.
55+
56+
- **Breaking:** reauthorize every message-reference status, result, and outcome
57+
read against the original invocation. Pass `authorization_context:` on every
58+
read.
59+
- **Breaking:** retain immutable JSON results for background and internal
60+
messages as well as synchronous calls. All operations now enforce result
61+
serialization and size limits; return `nil` explicitly when an operation does
62+
not need a result. `result` raises terminal rejection/failure errors;
63+
`outcome` exposes them as data.
64+
- Preserve polling transition intervals in milliseconds and string reasons in
65+
portable telemetry. Pin transmit staging order and null-argument validation
66+
against the shared JavaScript contract.
67+
68+
- Add portable telemetry, isolated observer hooks, metric definitions, and bounded authorized actor diagnostics matching JavaScript.
69+
370
## 0.16.1 - 2026-10-02
471

572
- Fix the SQLite join order of the claimed-message scan. The query in

‎Gemfile.lock‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
PATH
22
remote: .
33
specs:
4-
solid_objects (0.16.1)
4+
solid_objects (0.17.0)
55
actioncable (>= 7.1)
66
actionpack (>= 7.1)
77
actionview (>= 7.1)
@@ -384,7 +384,7 @@ CHECKSUMS
384384
rubocop-rails-omakase (1.1.0) sha256=2af73ac8ee5852de2919abbd2618af9c15c19b512c4cfc1f9a5d3b6ef009109d
385385
ruby-progressbar (1.13.0) sha256=80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33
386386
securerandom (0.4.1) sha256=cc5193d414a4341b6e225f0cb4446aceca8e50d5e1888743fac16987638ea0b1
387-
solid_objects (0.16.1)
387+
solid_objects (0.17.0)
388388
sqlite3 (2.9.5-aarch64-linux-gnu) sha256=78075b6337d3d182c6d2b4691049ed45cd220826160c9ea18946bf6a1de200dc
389389
sqlite3 (2.9.5-aarch64-linux-musl) sha256=18c801185deb4adc01ddb281e8f672a39e3d1729979ca91e39439cd3eac0402d
390390
sqlite3 (2.9.5-arm-linux-gnu) sha256=1bdfca0c7d63998c60b0f4a8e3c8df2d33800ccc4abd2d612eddbbbc92a4c48b

‎README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ Exactly once is not hiding in a more advanced configuration. Read the
162162
- [Five-minute Rails guide](https://solidobjects.dev/5min/rails)
163163
- [Choosing Solid Objects](docs/fit.md)
164164
- [Operations and recovery](docs/operations.md)
165+
- [Observability and diagnostics](docs/observability.md)
165166
- [Reminders](docs/reminders.md)
166167
- [Reactive ERB](docs/realtime.md)
167168
- [Detailed architecture](docs/architecture.md)

‎app/models/solid_objects/message.rb‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,21 @@ def dead?
4747
dead_letter.present?
4848
end
4949

50+
# @rbs () -> json_value
51+
def result!
52+
if rejected?
53+
raise Rejected.new(
54+
code: rejection.fetch("code"),
55+
message: rejection.fetch("message"),
56+
details: rejection.fetch("details"),
57+
message_id: id
58+
)
59+
end
60+
raise MessageFailed.new("actor message failed permanently", message_id: id, details: error || {}) if dead?
61+
62+
Serialization.readonly_copy(result)
63+
end
64+
5065
private
5166

5267
# @rbs () -> void

‎compatibility/json-values.json‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
[
2+
{
3+
"name": "object prototype key",
4+
"value": { "__proto__": { "role": "ordinary data" }, "value": 1 }
5+
},
6+
{ "name": "scalar prototype key", "value": { "__proto__": "ordinary data" } },
7+
{ "name": "null prototype key", "value": { "__proto__": null } },
8+
{
9+
"name": "nested reserved names",
10+
"value": {
11+
"items": [{ "__proto__": { "constructor": "data" }, "prototype": true, "hasOwnProperty": 1 }]
12+
}
13+
}
14+
]

‎compatibility/sync-timeout.json‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
[
2+
{ "rubyReason": "actor_paused", "waitingOn": "actorPaused" },
3+
{ "rubyReason": "activation_held", "waitingOn": "activationHeld" },
4+
{ "rubyReason": "earlier_message", "waitingOn": "earlierMessage" },
5+
{ "rubyReason": "message_claimed", "waitingOn": "messageClaimed" },
6+
{ "rubyReason": "not_yet_available", "waitingOn": "notYetAvailable" },
7+
{ "rubyReason": "ready_unclaimed", "waitingOn": "readyUnclaimed" },
8+
{ "rubyReason": "database_contention", "waitingOn": "databaseContention" },
9+
{ "rubyReason": "unknown", "waitingOn": "unknown" }
10+
]

0 commit comments

Comments
 (0)