solid_objects 0.14.1 → 0.14.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +83 -0
- data/README.md +115 -1299
- data/benchmark/state_size.rb +5 -0
- data/benchmark/support.rb +63 -0
- data/docs/adr/0006-at-least-once-delivery.md +1 -1
- data/docs/architecture.md +46 -6
- data/docs/authorization.md +4 -4
- data/docs/benchmarks.md +48 -3
- data/docs/correctness.md +2 -2
- data/docs/fit.md +24 -7
- data/docs/local-testing.md +3 -3
- data/docs/migrating-existing-state.md +1 -1
- data/docs/operations.md +178 -24
- data/docs/realtime.md +2 -2
- data/docs/reminders.md +112 -0
- data/docs/research/solid_queue.md +1 -1
- data/docs/roadmap.md +9 -1
- data/lib/solid_objects/configuration.rb +7 -0
- data/lib/solid_objects/executor.rb +37 -16
- data/lib/solid_objects/instrumentation.rb +33 -0
- data/lib/solid_objects/serialization.rb +18 -5
- data/lib/solid_objects/version.rb +1 -1
- data/sig/generated/lib/solid_objects/configuration.rbs +7 -3
- data/sig/generated/lib/solid_objects/executor.rbs +7 -4
- data/sig/generated/lib/solid_objects/instrumentation.rbs +11 -0
- data/sig/generated/lib/solid_objects/serialization.rbs +16 -0
- metadata +4 -2
data/docs/reminders.md
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# Reminders
|
|
2
|
+
|
|
3
|
+
Reminders are Solid Objects' durable equivalent of the Durable Objects Alarms
|
|
4
|
+
API. One-shot and recurring alarms are actor-owned database records:
|
|
5
|
+
|
|
6
|
+
```ruby
|
|
7
|
+
def schedule_evaluation
|
|
8
|
+
schedule(
|
|
9
|
+
at: 1.hour.from_now,
|
|
10
|
+
every: 1.hour,
|
|
11
|
+
missed: :latest
|
|
12
|
+
).evaluate(account_id:)
|
|
13
|
+
end
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Use `missed: :latest` to coalesce missed occurrences or `missed: :all` to
|
|
17
|
+
enqueue each one.
|
|
18
|
+
|
|
19
|
+
## A reminder is one named alarm per actor
|
|
20
|
+
|
|
21
|
+
The uniqueness key is `(actor, reminder name)`. Scheduling a name that is
|
|
22
|
+
already armed **moves the existing alarm** rather than adding a second one. The
|
|
23
|
+
database enforces this with a unique index on `(instance_id, name)`.
|
|
24
|
+
|
|
25
|
+
This is the same model as Orleans reminders and Durable Objects alarms. It
|
|
26
|
+
makes a reminder safe to re-arm from a handler that may run more than once.
|
|
27
|
+
Without a key the name is the operation, so this is a data-loss bug:
|
|
28
|
+
|
|
29
|
+
```ruby
|
|
30
|
+
# Wrong. Every entry overwrites the previous entry's alarm.
|
|
31
|
+
def add(entry:)
|
|
32
|
+
self.entries = entries + [ entry ]
|
|
33
|
+
schedule(at: entry.fetch("wait_until")).deliver
|
|
34
|
+
end
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Two entries leave one reminder. The earlier wake-up never happens, nothing
|
|
38
|
+
raises, and nothing is logged except a `solid_objects.reminder.replaced` event.
|
|
39
|
+
|
|
40
|
+
## An alarm per item, with `key:`
|
|
41
|
+
|
|
42
|
+
Pass `key:` when an actor is waiting on several things at once. The key is your
|
|
43
|
+
own identifier for the item, and it names that item's alarm, so each item gets
|
|
44
|
+
one:
|
|
45
|
+
|
|
46
|
+
```ruby
|
|
47
|
+
def add(entry:)
|
|
48
|
+
self.entries = entries + [ entry ]
|
|
49
|
+
schedule(at: entry.fetch("wait_until"), key: entry.fetch("id")).deliver
|
|
50
|
+
end
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Two entries now leave two reminders. Scheduling the same key again moves that
|
|
54
|
+
item's alarm and leaves the others alone, so a keyed reminder is as safe to
|
|
55
|
+
re-arm as an unkeyed one. The operation still decides which handler runs; the
|
|
56
|
+
key only decides which alarm is which.
|
|
57
|
+
|
|
58
|
+
A key must be non-empty, and the name it becomes must fit the 191-character
|
|
59
|
+
column, which is checked on the composed name rather than the key alone so a
|
|
60
|
+
long operation and a short key are caught too.
|
|
61
|
+
|
|
62
|
+
The key is separated from the operation by a colon, so an operation may not hold
|
|
63
|
+
one. Otherwise an unkeyed `deliver:item` and a `deliver` keyed `item` would be
|
|
64
|
+
one name, and the second would silently take the first one's alarm. A key may
|
|
65
|
+
hold colons of its own, because the operation before the first one cannot.
|
|
66
|
+
|
|
67
|
+
## One alarm for a whole queue
|
|
68
|
+
|
|
69
|
+
A key per item is not always what you want. An actor that only ever needs to
|
|
70
|
+
know "what is next" can keep one alarm and let the handler drain everything now
|
|
71
|
+
due before arming the next:
|
|
72
|
+
|
|
73
|
+
```ruby
|
|
74
|
+
def add(entry:)
|
|
75
|
+
self.entries = (entries + [ entry ]).sort_by { |item| item.fetch("wait_until") }
|
|
76
|
+
arm_next
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def deliver
|
|
80
|
+
now = Time.current.to_i
|
|
81
|
+
due, pending = entries.partition { |item| item.fetch("wait_until") <= now }
|
|
82
|
+
due.each { |item| emit :send_push, **item.symbolize_keys }
|
|
83
|
+
self.entries = pending
|
|
84
|
+
arm_next
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
private
|
|
88
|
+
|
|
89
|
+
def arm_next
|
|
90
|
+
earliest = entries.first
|
|
91
|
+
return unless earliest
|
|
92
|
+
|
|
93
|
+
schedule(at: Time.at(earliest.fetch("wait_until"))).deliver
|
|
94
|
+
end
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
That costs one reminder row instead of one per item, and a coalesced occurrence
|
|
98
|
+
cannot strand an entry because the handler drains by time rather than by alarm.
|
|
99
|
+
Prefer it when the queue is large and the items are interchangeable; prefer
|
|
100
|
+
`key:` when an item needs its own alarm that can be moved on its own.
|
|
101
|
+
|
|
102
|
+
Solid Objects has no `unschedule`. A reminder stops when its handler does not
|
|
103
|
+
re-arm it, and destroying an actor removes its reminders.
|
|
104
|
+
|
|
105
|
+
Self-scheduling actors should also have a low-frequency application reconciler.
|
|
106
|
+
It may read `SolidObjects::Instance.states_for`, `.without_pending_work`, and
|
|
107
|
+
`.orphaned`, but every repair must go through `async`. Never bulk-update actor
|
|
108
|
+
state around the lease and fencing checks.
|
|
109
|
+
|
|
110
|
+
Suspended actors should be reported rather than silently resumed. Spread large
|
|
111
|
+
repair batches with `available_at:` so reconciliation cannot stampede one
|
|
112
|
+
mailbox or the worker fleet.
|
|
@@ -472,7 +472,7 @@ The current `cardmagic/classifier` source uses RBS::Inline directly in Ruby file
|
|
|
472
472
|
- [`.github/workflows/ruby.yml`](https://github.com/cardmagic/classifier/blob/48cdfa63f3efdba8149c8f47dd053ceebce5dfc1/.github/workflows/ruby.yml) generates signatures, validates them with RBS, and runs Steep.
|
|
473
473
|
- [`Steepfile`](https://github.com/cardmagic/classifier/blob/48cdfa63f3efdba8149c8f47dd053ceebce5dfc1/Steepfile) enables strict diagnostics for the typed library while explicitly isolating incompatible extension files.
|
|
474
474
|
|
|
475
|
-
Solid Objects will use the same source-adjacent convention. Every owned Ruby source file starts with `# rbs_inline: enabled`, declares its instance variables, and annotates public and private methods.
|
|
475
|
+
Solid Objects will use the same source-adjacent convention. Every owned Ruby source file starts with `# rbs_inline: enabled`, declares its instance variables, and annotates public and private methods. The build checks the generated signatures. Nobody maintains them by hand as a second authority.
|
|
476
476
|
|
|
477
477
|
## Related primary-source findings
|
|
478
478
|
|
data/docs/roadmap.md
CHANGED
|
@@ -134,7 +134,15 @@
|
|
|
134
134
|
loads them in every process, and a rejected subscription reports which
|
|
135
135
|
condition caused it instead of closing the socket silently.
|
|
136
136
|
- Backpressure: mailbox/payload/state/result caps and fair yields exist;
|
|
137
|
-
distributed per-actor rate limits and global admission control do not.
|
|
137
|
+
distributed per-actor rate limits and global admission control do not. The
|
|
138
|
+
state cap is a limit rather than an operating point. `max_state_bytes`
|
|
139
|
+
defaults to 5 MB, and committed throughput measured on SQLite falls about 53
|
|
140
|
+
times between an empty state and 1 MB of state, which `docs/benchmarks.md`
|
|
141
|
+
records. A soft `warn_state_bytes` threshold, 64 KB by default, now
|
|
142
|
+
reports each commit above it as `solid_objects.state.large`. The hard default
|
|
143
|
+
stays at 5 MB, because lowering it would break an application whose actors
|
|
144
|
+
already exceed a lower value; a major release can lower it from the measured
|
|
145
|
+
curve.
|
|
138
146
|
- Administration: `SolidObjects::Web` is a mountable Rack dashboard covering
|
|
139
147
|
instances, mailbox, reminders, effects, broadcasts, dead letters, and
|
|
140
148
|
processes, with actor-type and actor-id filtering, status filters, paging, a
|
|
@@ -15,6 +15,7 @@ module SolidObjects
|
|
|
15
15
|
# @rbs @claim_scan_limit: Integer
|
|
16
16
|
# @rbs @max_payload_bytes: Integer
|
|
17
17
|
# @rbs @max_state_bytes: Integer
|
|
18
|
+
# @rbs @warn_state_bytes: Integer
|
|
18
19
|
# @rbs @max_result_bytes: Integer
|
|
19
20
|
# @rbs @max_attempts: Integer
|
|
20
21
|
# @rbs @retry_delay: Proc
|
|
@@ -63,6 +64,7 @@ module SolidObjects
|
|
|
63
64
|
:claim_scan_limit,
|
|
64
65
|
:max_payload_bytes,
|
|
65
66
|
:max_state_bytes,
|
|
67
|
+
:warn_state_bytes,
|
|
66
68
|
:max_result_bytes,
|
|
67
69
|
:max_attempts,
|
|
68
70
|
:retry_delay,
|
|
@@ -116,6 +118,7 @@ module SolidObjects
|
|
|
116
118
|
@claim_scan_limit = 100
|
|
117
119
|
@max_payload_bytes = 1.megabyte
|
|
118
120
|
@max_state_bytes = 5.megabytes
|
|
121
|
+
@warn_state_bytes = 64.kilobytes
|
|
119
122
|
@max_result_bytes = 1.megabyte
|
|
120
123
|
@max_attempts = 5
|
|
121
124
|
@retry_delay = ->(attempt) { [ 2**(attempt - 1), 60 ].min.to_f }
|
|
@@ -223,6 +226,9 @@ module SolidObjects
|
|
|
223
226
|
positive_values.each do |name, value|
|
|
224
227
|
raise ArgumentError, "#{name} must be positive" unless value.positive?
|
|
225
228
|
end
|
|
229
|
+
if warn_state_bytes > max_state_bytes
|
|
230
|
+
raise ArgumentError, "warn_state_bytes must not exceed max_state_bytes"
|
|
231
|
+
end
|
|
226
232
|
message_retention_by_actor_type.each do |actor_type, retention|
|
|
227
233
|
raise ArgumentError, "actor type cannot be empty" if actor_type.to_s.empty?
|
|
228
234
|
raise ArgumentError, "message retention must be positive" unless retention.positive?
|
|
@@ -260,6 +266,7 @@ module SolidObjects
|
|
|
260
266
|
claim_scan_limit:,
|
|
261
267
|
max_payload_bytes:,
|
|
262
268
|
max_state_bytes:,
|
|
269
|
+
warn_state_bytes:,
|
|
263
270
|
max_result_bytes:,
|
|
264
271
|
max_attempts:,
|
|
265
272
|
process_heartbeat_interval:,
|
|
@@ -25,9 +25,15 @@ module SolidObjects
|
|
|
25
25
|
|
|
26
26
|
SolidObjects.instrument(:"message.started", **instrumentation_payload)
|
|
27
27
|
result = invoke_actor(message_context)
|
|
28
|
-
ensure_query_did_not_mutate_state!(state_before)
|
|
29
28
|
observable_changes = changed_observables(observables_before, actor.observable_values)
|
|
30
|
-
|
|
29
|
+
state_after = actor.state.to_h
|
|
30
|
+
ensure_query_did_not_mutate_state!(state_before, state_after)
|
|
31
|
+
complete(
|
|
32
|
+
result,
|
|
33
|
+
observable_changes,
|
|
34
|
+
state_after:,
|
|
35
|
+
state_changed: state_after != state_before
|
|
36
|
+
)
|
|
31
37
|
true
|
|
32
38
|
rescue LostActivation
|
|
33
39
|
raise
|
|
@@ -59,11 +65,11 @@ module SolidObjects
|
|
|
59
65
|
end
|
|
60
66
|
end
|
|
61
67
|
|
|
62
|
-
# @rbs (Hash[String, untyped]) -> void
|
|
63
|
-
def ensure_query_did_not_mutate_state!(state_before)
|
|
68
|
+
# @rbs (Hash[String, untyped], Hash[String, untyped]) -> void
|
|
69
|
+
def ensure_query_did_not_mutate_state!(state_before, state_after)
|
|
64
70
|
return unless message.delivery_mode == "sync"
|
|
65
71
|
return unless actor.class.definition.queries.key?(message.operation.to_sym)
|
|
66
|
-
return if
|
|
72
|
+
return if state_after == state_before
|
|
67
73
|
|
|
68
74
|
raise InvalidActor, "query #{message.operation.inspect} mutated actor state"
|
|
69
75
|
end
|
|
@@ -75,10 +81,10 @@ module SolidObjects
|
|
|
75
81
|
end
|
|
76
82
|
end
|
|
77
83
|
|
|
78
|
-
# @rbs (untyped, Hash[String, untyped], state_changed: bool) -> void
|
|
79
|
-
def complete(result, observable_changes, state_changed:)
|
|
80
|
-
|
|
81
|
-
|
|
84
|
+
# @rbs (untyped, Hash[String, untyped], state_after: Hash[String, untyped], state_changed: bool) -> void
|
|
85
|
+
def complete(result, observable_changes, state_after:, state_changed:)
|
|
86
|
+
dumped_state = Serialization.dump_with_byte_size(
|
|
87
|
+
state_after,
|
|
82
88
|
max_bytes: SolidObjects.configuration.max_state_bytes
|
|
83
89
|
)
|
|
84
90
|
serialized_result = Serialization.dump(
|
|
@@ -102,7 +108,7 @@ module SolidObjects
|
|
|
102
108
|
locked_message = Message.lock.find(message.id)
|
|
103
109
|
execute_commit_actions(commit_action_intents)
|
|
104
110
|
instance.update!(
|
|
105
|
-
state:
|
|
111
|
+
state: dumped_state.value,
|
|
106
112
|
state_version: actor.class.state_version,
|
|
107
113
|
state_revision: locked_message.sequence,
|
|
108
114
|
last_used_at: SolidObjects.database_adapter.database_now
|
|
@@ -129,17 +135,17 @@ module SolidObjects
|
|
|
129
135
|
end
|
|
130
136
|
|
|
131
137
|
observable_changes.each_key do |observable_name|
|
|
132
|
-
SolidObjects.
|
|
138
|
+
SolidObjects.instrument_after_commit(
|
|
133
139
|
:"broadcast.enqueued",
|
|
134
140
|
**instrumentation_payload,
|
|
135
141
|
observable_name:
|
|
136
142
|
)
|
|
137
143
|
end
|
|
138
144
|
moved_reminders.each do |moved|
|
|
139
|
-
SolidObjects.
|
|
145
|
+
SolidObjects.instrument_after_commit(:"reminder.replaced", **moved)
|
|
140
146
|
end
|
|
141
147
|
enqueued_effects.each do |effect|
|
|
142
|
-
SolidObjects.
|
|
148
|
+
SolidObjects.instrument_after_commit(
|
|
143
149
|
:"effect.enqueued",
|
|
144
150
|
effect_id: effect.effect_id,
|
|
145
151
|
effect_name: effect.name,
|
|
@@ -148,10 +154,25 @@ module SolidObjects
|
|
|
148
154
|
actor_id: message.actor_id
|
|
149
155
|
)
|
|
150
156
|
end
|
|
151
|
-
|
|
157
|
+
report_large_state(dumped_state.byte_size)
|
|
158
|
+
SolidObjects.instrument_after_commit(:"message.completed", **instrumentation_payload)
|
|
152
159
|
SolidObjects.wake_up.signal
|
|
153
160
|
end
|
|
154
161
|
|
|
162
|
+
# @rbs (Integer) -> void
|
|
163
|
+
def report_large_state(byte_count)
|
|
164
|
+
threshold = SolidObjects.configuration.warn_state_bytes
|
|
165
|
+
return if byte_count <= threshold
|
|
166
|
+
|
|
167
|
+
SolidObjects.instrument_after_commit(
|
|
168
|
+
:"state.large",
|
|
169
|
+
actor_type: message.actor_type,
|
|
170
|
+
actor_id: message.actor_id,
|
|
171
|
+
byte_count:,
|
|
172
|
+
threshold_bytes: threshold
|
|
173
|
+
)
|
|
174
|
+
end
|
|
175
|
+
|
|
155
176
|
# @rbs (Array[Actor::CommitActionIntent]) -> void
|
|
156
177
|
def execute_commit_actions(intents)
|
|
157
178
|
ensure_application_database_is_shared! if intents.any?
|
|
@@ -352,7 +373,7 @@ module SolidObjects
|
|
|
352
373
|
end
|
|
353
374
|
end
|
|
354
375
|
|
|
355
|
-
SolidObjects.
|
|
376
|
+
SolidObjects.instrument_after_commit(
|
|
356
377
|
:"message.failed",
|
|
357
378
|
**instrumentation_payload,
|
|
358
379
|
error_class: error.class.name,
|
|
@@ -387,7 +408,7 @@ module SolidObjects
|
|
|
387
408
|
claimed_message.destroy!
|
|
388
409
|
end
|
|
389
410
|
|
|
390
|
-
SolidObjects.
|
|
411
|
+
SolidObjects.instrument_after_commit(
|
|
391
412
|
:"message.rejected",
|
|
392
413
|
**instrumentation_payload,
|
|
393
414
|
code: rejection.code
|
|
@@ -6,5 +6,38 @@ module SolidObjects
|
|
|
6
6
|
def instrument(event, **payload, &block)
|
|
7
7
|
ActiveSupport::Notifications.instrument("solid_objects.#{event}", payload, &block)
|
|
8
8
|
end
|
|
9
|
+
|
|
10
|
+
# @rbs (Symbol, **untyped) -> void
|
|
11
|
+
def instrument_after_commit(event, **payload)
|
|
12
|
+
instrument(event, **payload)
|
|
13
|
+
rescue => error
|
|
14
|
+
report_instrumentation_failure(event, error)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
# @rbs (Symbol, Exception) -> void
|
|
20
|
+
def report_instrumentation_failure(event, error)
|
|
21
|
+
instrument(
|
|
22
|
+
:"instrumentation.failed",
|
|
23
|
+
instrumentation_event: "solid_objects.#{event}",
|
|
24
|
+
error_class: error.class.name
|
|
25
|
+
)
|
|
26
|
+
rescue => failure
|
|
27
|
+
log_instrumentation_failure(event, failure)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# @rbs (Symbol, Exception) -> void
|
|
31
|
+
def log_instrumentation_failure(event, error)
|
|
32
|
+
SolidObjects.configuration.logger.error(
|
|
33
|
+
{
|
|
34
|
+
event: "solid_objects.instrumentation.failed",
|
|
35
|
+
instrumentation_event: "solid_objects.#{event}",
|
|
36
|
+
error_class: error.class.name
|
|
37
|
+
}
|
|
38
|
+
)
|
|
39
|
+
rescue
|
|
40
|
+
nil
|
|
41
|
+
end
|
|
9
42
|
end
|
|
10
43
|
end
|
|
@@ -4,17 +4,26 @@ module SolidObjects
|
|
|
4
4
|
module Serialization
|
|
5
5
|
MAX_NESTING = 100
|
|
6
6
|
|
|
7
|
+
Dumped = Data.define(:value, :byte_size)
|
|
8
|
+
|
|
7
9
|
class << self
|
|
8
10
|
# @rbs (untyped, ?max_bytes: Integer?) -> untyped
|
|
9
11
|
def dump(value, max_bytes: nil)
|
|
12
|
+
return normalize(value) unless max_bytes
|
|
13
|
+
|
|
14
|
+
dump_with_byte_size(value, max_bytes:).value
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# @rbs (untyped, ?max_bytes: Integer?) -> Dumped
|
|
18
|
+
def dump_with_byte_size(value, max_bytes: nil)
|
|
10
19
|
normalized = normalize(value)
|
|
11
|
-
|
|
20
|
+
byte_size = JSON.generate(normalized, max_nesting: MAX_NESTING).bytesize
|
|
12
21
|
|
|
13
|
-
if max_bytes &&
|
|
22
|
+
if max_bytes && byte_size > max_bytes
|
|
14
23
|
raise PayloadTooLarge, "serialized value exceeds #{max_bytes} bytes"
|
|
15
24
|
end
|
|
16
25
|
|
|
17
|
-
normalized
|
|
26
|
+
Dumped.new(value: normalized, byte_size:)
|
|
18
27
|
rescue JSON::GeneratorError, EncodingError => error
|
|
19
28
|
raise InvalidPayload, error.message
|
|
20
29
|
end
|
|
@@ -60,7 +69,11 @@ module SolidObjects
|
|
|
60
69
|
raise InvalidPayload, "serialized value is nested too deeply" if depth > MAX_NESTING
|
|
61
70
|
|
|
62
71
|
case value
|
|
63
|
-
when nil, true, false,
|
|
72
|
+
when nil, true, false, Integer
|
|
73
|
+
value
|
|
74
|
+
when String
|
|
75
|
+
raise InvalidPayload, "strings must hold valid #{value.encoding} bytes" unless value.valid_encoding?
|
|
76
|
+
|
|
64
77
|
value
|
|
65
78
|
when Float
|
|
66
79
|
raise InvalidPayload, "non-finite numbers are not supported" unless value.finite?
|
|
@@ -89,7 +102,7 @@ module SolidObjects
|
|
|
89
102
|
|
|
90
103
|
# @rbs (untyped) -> String
|
|
91
104
|
def normalize_key(key)
|
|
92
|
-
return key if key.is_a?(String)
|
|
105
|
+
return normalize(key) if key.is_a?(String)
|
|
93
106
|
return key.to_s if key.is_a?(Symbol)
|
|
94
107
|
|
|
95
108
|
raise InvalidPayload, "JSON object keys must be strings or symbols"
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module SolidObjects
|
|
4
4
|
class Configuration
|
|
5
|
-
@
|
|
5
|
+
@process_alive_threshold: Float
|
|
6
6
|
|
|
7
7
|
@shutdown_timeout: Float
|
|
8
8
|
|
|
@@ -60,6 +60,8 @@ module SolidObjects
|
|
|
60
60
|
|
|
61
61
|
@transmission_actor_type_resolver: Proc
|
|
62
62
|
|
|
63
|
+
@table_name_prefix: String
|
|
64
|
+
|
|
63
65
|
@polling_interval: Float
|
|
64
66
|
|
|
65
67
|
@idle_polling_interval: Float
|
|
@@ -84,6 +86,8 @@ module SolidObjects
|
|
|
84
86
|
|
|
85
87
|
@max_state_bytes: Integer
|
|
86
88
|
|
|
89
|
+
@warn_state_bytes: Integer
|
|
90
|
+
|
|
87
91
|
@max_result_bytes: Integer
|
|
88
92
|
|
|
89
93
|
@max_attempts: Integer
|
|
@@ -94,8 +98,6 @@ module SolidObjects
|
|
|
94
98
|
|
|
95
99
|
@process_heartbeat_interval: Float
|
|
96
100
|
|
|
97
|
-
@process_alive_threshold: Float
|
|
98
|
-
|
|
99
101
|
attr_accessor table_name_prefix: untyped
|
|
100
102
|
|
|
101
103
|
attr_accessor polling_interval: untyped
|
|
@@ -122,6 +124,8 @@ module SolidObjects
|
|
|
122
124
|
|
|
123
125
|
attr_accessor max_state_bytes: untyped
|
|
124
126
|
|
|
127
|
+
attr_accessor warn_state_bytes: untyped
|
|
128
|
+
|
|
125
129
|
attr_accessor max_result_bytes: untyped
|
|
126
130
|
|
|
127
131
|
attr_accessor max_attempts: untyped
|
|
@@ -24,14 +24,17 @@ module SolidObjects
|
|
|
24
24
|
# @rbs (MessageContext) -> untyped
|
|
25
25
|
def invoke_actor: (MessageContext) -> untyped
|
|
26
26
|
|
|
27
|
-
# @rbs (Hash[String, untyped]) -> void
|
|
28
|
-
def ensure_query_did_not_mutate_state!: (Hash[String, untyped]) -> void
|
|
27
|
+
# @rbs (Hash[String, untyped], Hash[String, untyped]) -> void
|
|
28
|
+
def ensure_query_did_not_mutate_state!: (Hash[String, untyped], Hash[String, untyped]) -> void
|
|
29
29
|
|
|
30
30
|
# @rbs (Hash[String, untyped], Hash[String, untyped]) -> Hash[String, untyped]
|
|
31
31
|
def changed_observables: (Hash[String, untyped], Hash[String, untyped]) -> Hash[String, untyped]
|
|
32
32
|
|
|
33
|
-
# @rbs (untyped, Hash[String, untyped], state_changed: bool) -> void
|
|
34
|
-
def complete: (untyped, Hash[String, untyped], state_changed: bool) -> void
|
|
33
|
+
# @rbs (untyped, Hash[String, untyped], state_after: Hash[String, untyped], state_changed: bool) -> void
|
|
34
|
+
def complete: (untyped, Hash[String, untyped], state_after: Hash[String, untyped], state_changed: bool) -> void
|
|
35
|
+
|
|
36
|
+
# @rbs (Integer) -> void
|
|
37
|
+
def report_large_state: (Integer) -> void
|
|
35
38
|
|
|
36
39
|
# @rbs (Array[Actor::CommitActionIntent]) -> void
|
|
37
40
|
def execute_commit_actions: (Array[Actor::CommitActionIntent]) -> void
|
|
@@ -4,5 +4,16 @@ module SolidObjects
|
|
|
4
4
|
module Instrumentation
|
|
5
5
|
# @rbs (Symbol, **untyped) { (Hash[Symbol, untyped]) -> untyped } -> untyped
|
|
6
6
|
def instrument: (Symbol, **untyped) { (Hash[Symbol, untyped]) -> untyped } -> untyped
|
|
7
|
+
|
|
8
|
+
# @rbs (Symbol, **untyped) -> void
|
|
9
|
+
def instrument_after_commit: (Symbol, **untyped) -> void
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
# @rbs (Symbol, Exception) -> void
|
|
14
|
+
def report_instrumentation_failure: (Symbol, Exception) -> void
|
|
15
|
+
|
|
16
|
+
# @rbs (Symbol, Exception) -> void
|
|
17
|
+
def log_instrumentation_failure: (Symbol, Exception) -> void
|
|
7
18
|
end
|
|
8
19
|
end
|
|
@@ -4,9 +4,25 @@ module SolidObjects
|
|
|
4
4
|
module Serialization
|
|
5
5
|
MAX_NESTING: ::Integer
|
|
6
6
|
|
|
7
|
+
class Dumped < Data
|
|
8
|
+
attr_reader value(): untyped
|
|
9
|
+
|
|
10
|
+
attr_reader byte_size(): untyped
|
|
11
|
+
|
|
12
|
+
def self.new: (untyped value, untyped byte_size) -> instance
|
|
13
|
+
| (value: untyped, byte_size: untyped) -> instance
|
|
14
|
+
|
|
15
|
+
def self.members: () -> [ :value, :byte_size ]
|
|
16
|
+
|
|
17
|
+
def members: () -> [ :value, :byte_size ]
|
|
18
|
+
end
|
|
19
|
+
|
|
7
20
|
# @rbs (untyped, ?max_bytes: Integer?) -> untyped
|
|
8
21
|
def self.dump: (untyped, ?max_bytes: Integer?) -> untyped
|
|
9
22
|
|
|
23
|
+
# @rbs (untyped, ?max_bytes: Integer?) -> Dumped
|
|
24
|
+
def self.dump_with_byte_size: (untyped, ?max_bytes: Integer?) -> Dumped
|
|
25
|
+
|
|
10
26
|
# @rbs (untyped) -> untyped
|
|
11
27
|
def self.load: (untyped) -> untyped
|
|
12
28
|
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: solid_objects
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.14.
|
|
4
|
+
version: 0.14.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Lucas Carlson
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-08-
|
|
11
|
+
date: 2026-08-29 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: actioncable
|
|
@@ -323,6 +323,7 @@ files:
|
|
|
323
323
|
- benchmark/idle_polling.rb
|
|
324
324
|
- benchmark/processing.rb
|
|
325
325
|
- benchmark/query_count.rb
|
|
326
|
+
- benchmark/state_size.rb
|
|
326
327
|
- benchmark/support.rb
|
|
327
328
|
- benchmark/sync_latency.rb
|
|
328
329
|
- config/routes.rb
|
|
@@ -355,6 +356,7 @@ files:
|
|
|
355
356
|
- docs/migrating-existing-state.md
|
|
356
357
|
- docs/operations.md
|
|
357
358
|
- docs/realtime.md
|
|
359
|
+
- docs/reminders.md
|
|
358
360
|
- docs/research/solid_queue.md
|
|
359
361
|
- docs/roadmap.md
|
|
360
362
|
- docs/security.md
|