solid_objects 0.13.0 → 0.13.2

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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +37 -0
  3. data/README.md +49 -10
  4. data/benchmark/idle_polling.rb +98 -0
  5. data/benchmark/support.rb +2 -0
  6. data/docs/adr/0011-wake-up-strategy.md +6 -0
  7. data/docs/architecture.md +2 -2
  8. data/docs/benchmarks.md +38 -0
  9. data/docs/development.md +1 -0
  10. data/docs/operations.md +32 -0
  11. data/docs/roadmap.md +8 -5
  12. data/lib/solid_objects/actor.rb +54 -4
  13. data/lib/solid_objects/administration.rb +44 -0
  14. data/lib/solid_objects/broadcast_executor.rb +35 -4
  15. data/lib/solid_objects/configuration.rb +4 -0
  16. data/lib/solid_objects/effect_executor.rb +34 -3
  17. data/lib/solid_objects/executor.rb +5 -5
  18. data/lib/solid_objects/polling_backoff.rb +45 -0
  19. data/lib/solid_objects/process_registry.rb +51 -0
  20. data/lib/solid_objects/reminder_scheduler.rb +35 -4
  21. data/lib/solid_objects/version.rb +1 -1
  22. data/lib/solid_objects/wake_up.rb +36 -4
  23. data/lib/solid_objects/wake_up_adapters/postgresql.rb +6 -0
  24. data/lib/solid_objects/wake_up_adapters/redis.rb +25 -3
  25. data/lib/solid_objects/worker.rb +39 -3
  26. data/lib/solid_objects.rb +9 -0
  27. data/sig/generated/lib/solid_objects/actor.rbs +35 -6
  28. data/sig/generated/lib/solid_objects/administration.rbs +13 -0
  29. data/sig/generated/lib/solid_objects/broadcast_executor.rbs +7 -0
  30. data/sig/generated/lib/solid_objects/configuration.rbs +6 -2
  31. data/sig/generated/lib/solid_objects/effect_executor.rbs +7 -0
  32. data/sig/generated/lib/solid_objects/executor.rbs +4 -4
  33. data/sig/generated/lib/solid_objects/polling_backoff.rbs +29 -0
  34. data/sig/generated/lib/solid_objects/process_registry.rbs +15 -0
  35. data/sig/generated/lib/solid_objects/reminder_scheduler.rbs +7 -0
  36. data/sig/generated/lib/solid_objects/wake_up.rbs +19 -2
  37. data/sig/generated/lib/solid_objects/wake_up_adapters/postgresql.rbs +3 -0
  38. data/sig/generated/lib/solid_objects/wake_up_adapters/redis.rbs +17 -2
  39. data/sig/generated/lib/solid_objects/worker.rbs +7 -0
  40. data/sig/generated/lib/solid_objects.rbs +3 -0
  41. metadata +7 -2
@@ -4,6 +4,7 @@ module SolidObjects
4
4
  class Configuration
5
5
  # @rbs @table_name_prefix: String
6
6
  # @rbs @polling_interval: Float
7
+ # @rbs @idle_polling_interval: Float
7
8
  # @rbs @sync_polling_interval: Float
8
9
  # @rbs @lease_duration: Float
9
10
  # @rbs @lease_renewal_interval: Float
@@ -49,6 +50,7 @@ module SolidObjects
49
50
 
50
51
  attr_accessor :table_name_prefix,
51
52
  :polling_interval,
53
+ :idle_polling_interval,
52
54
  :sync_polling_interval,
53
55
  :lease_duration,
54
56
  :lease_renewal_interval,
@@ -99,6 +101,7 @@ module SolidObjects
99
101
  def initialize
100
102
  @table_name_prefix = "solid_objects_"
101
103
  @polling_interval = 0.1
104
+ @idle_polling_interval = 1.0
102
105
  @sync_polling_interval = 0.05
103
106
  @lease_duration = 30.0
104
107
  @lease_renewal_interval = 10.0
@@ -241,6 +244,7 @@ module SolidObjects
241
244
  def positive_values
242
245
  {
243
246
  polling_interval:,
247
+ idle_polling_interval:,
244
248
  sync_polling_interval:,
245
249
  lease_duration:,
246
250
  lease_renewal_interval:,
@@ -1,5 +1,7 @@
1
1
  # rbs_inline: enabled
2
2
 
3
+ require "solid_objects/polling_backoff"
4
+
3
5
  module SolidObjects
4
6
  EffectContext = Data.define(:id, :attempt, :source_message_id, :actor_type, :actor_id)
5
7
 
@@ -10,6 +12,7 @@ module SolidObjects
10
12
  # @rbs @database_adapter: DatabaseAdapter
11
13
  # @rbs @stopped: bool
12
14
  # @rbs @shutdown_requested: bool
15
+ # @rbs @polling_backoff: PollingBackoff
13
16
 
14
17
  # @rbs (?process_registry: ProcessRegistry, ?database_adapter: DatabaseAdapter) -> void
15
18
  def initialize(
@@ -21,6 +24,17 @@ module SolidObjects
21
24
  process_registry.register(kind: "effect")
22
25
  @stopped = false
23
26
  @shutdown_requested = false
27
+ @polling_backoff = PollingBackoff.new(
28
+ minimum_interval: SolidObjects.configuration.polling_interval,
29
+ maximum_interval: SolidObjects.configuration.idle_polling_interval,
30
+ on_change: ->(transition) do
31
+ SolidObjects.instrument(
32
+ :"polling.interval_changed",
33
+ role: "effects",
34
+ **transition
35
+ )
36
+ end
37
+ )
24
38
  end
25
39
 
26
40
  # @rbs () -> bool
@@ -50,11 +64,23 @@ module SolidObjects
50
64
 
51
65
  # @rbs () -> void
52
66
  def run
67
+ ProcessRegistry.warn_if_polling_is_only_cross_process_wake_up
68
+
53
69
  until shutdown_requested?
70
+ wake_up = SolidObjects.wake_up
71
+ watch = wake_up.respond_to?(:watch) ? wake_up.watch : wake_up
54
72
  worked = run_once
55
- next if worked
73
+ if worked
74
+ polling_backoff.reset(:work)
75
+ next
76
+ end
56
77
 
57
- SolidObjects.wake_up.wait(timeout: SolidObjects.configuration.polling_interval)
78
+ notified = watch.wait(timeout: current_polling_interval)
79
+ if notified == false
80
+ polling_backoff.record_idle
81
+ else
82
+ polling_backoff.reset(:wake_up)
83
+ end
58
84
  end
59
85
  ensure
60
86
  stop
@@ -76,9 +102,14 @@ module SolidObjects
76
102
  @shutdown_requested
77
103
  end
78
104
 
105
+ # @rbs () -> Float
106
+ def current_polling_interval
107
+ polling_backoff.current_interval
108
+ end
109
+
79
110
  private
80
111
 
81
- attr_reader :process_registry, :database_adapter
112
+ attr_reader :process_registry, :database_adapter, :polling_backoff
82
113
 
83
114
  # @rbs () -> Effect?
84
115
  def claim_next
@@ -223,10 +223,10 @@ module SolidObjects
223
223
  end
224
224
 
225
225
  # A reminder is one named alarm per actor, so scheduling a name that is
226
- # already armed moves it rather than adding a second. An actor that arms a
227
- # reminder per queued item therefore keeps only the last, and nothing else
228
- # about that is visible: the write succeeds and the earlier wake-up simply
229
- # never happens.
226
+ # already armed moves it rather than adding a second. Without a key that
227
+ # name is the operation, so an actor arming a reminder per queued item keeps
228
+ # only the last; passing schedule a key gives each item its own name and so
229
+ # its own alarm.
230
230
  # Moves are returned rather than reported here, so the report happens after
231
231
  # the turn commits. A rolled back turn would otherwise announce an alarm
232
232
  # that never moved, which is the opposite of the visibility this event
@@ -239,7 +239,7 @@ module SolidObjects
239
239
  reminder.assign_attributes(
240
240
  actor_type: instance.actor_type,
241
241
  actor_id: instance.actor_id,
242
- operation: intent.name,
242
+ operation: intent.operation,
243
243
  arguments: intent.arguments,
244
244
  next_run_at: intent.at,
245
245
  interval_seconds: intent.interval_seconds,
@@ -0,0 +1,45 @@
1
+ # rbs_inline: enabled
2
+
3
+ module SolidObjects
4
+ class PollingBackoff
5
+ # @rbs @minimum_interval: Float
6
+ # @rbs @maximum_interval: Float
7
+ # @rbs @current_interval: Float
8
+ # @rbs @on_change: Proc?
9
+
10
+ attr_reader :current_interval
11
+
12
+ # @rbs (minimum_interval: Numeric, maximum_interval: Numeric, ?on_change: Proc?) -> void
13
+ def initialize(minimum_interval:, maximum_interval:, on_change: nil)
14
+ @minimum_interval = minimum_interval.to_f
15
+ @maximum_interval = [ @minimum_interval, maximum_interval.to_f ].max
16
+ @current_interval = @minimum_interval
17
+ @on_change = on_change
18
+ end
19
+
20
+ # @rbs () -> void
21
+ def record_idle
22
+ change([ current_interval * 2, @maximum_interval ].min, :idle)
23
+ end
24
+
25
+ # @rbs (:work | :wake_up) -> void
26
+ def reset(reason)
27
+ change(@minimum_interval, reason)
28
+ end
29
+
30
+ private
31
+
32
+ # @rbs (Float, :idle | :work | :wake_up) -> void
33
+ def change(interval, reason)
34
+ return if interval == current_interval
35
+
36
+ previous_interval = current_interval
37
+ @current_interval = interval
38
+ @on_change&.call(
39
+ previous_interval:,
40
+ current_interval: interval,
41
+ reason:
42
+ )
43
+ end
44
+ end
45
+ end
@@ -4,6 +4,9 @@ require "socket"
4
4
 
5
5
  module SolidObjects
6
6
  class ProcessRegistry
7
+ @polling_warning_mutex = Mutex.new
8
+ @polling_warning_emitted = false
9
+
7
10
  class << self
8
11
  # @rbs (?now: Time) -> Integer
9
12
  def cleanup_dead(now: SolidObjects.database_adapter.database_now)
@@ -53,8 +56,56 @@ module SolidObjects
53
56
  true
54
57
  end
55
58
 
59
+ # @rbs () -> void
60
+ def warn_if_polling_is_only_cross_process_wake_up
61
+ return if SolidObjects.configuration.wake_up_adapter
62
+
63
+ polling_warning_mutex.synchronize do
64
+ return if polling_warning_emitted?
65
+ return unless another_live_process?
66
+
67
+ payload = {
68
+ event: "solid_objects.polling_only_cross_process_wake_up",
69
+ polling_interval: SolidObjects.configuration.polling_interval,
70
+ idle_polling_interval: SolidObjects.configuration.idle_polling_interval
71
+ }
72
+ SolidObjects.configuration.logger.warn(payload)
73
+ SolidObjects.instrument(
74
+ :"polling.only_cross_process_wake_up",
75
+ **payload.except(:event)
76
+ )
77
+ @polling_warning_emitted = true
78
+ end
79
+ end
80
+
81
+ # @rbs () -> void
82
+ def reset_polling_warning!
83
+ polling_warning_mutex.synchronize { @polling_warning_emitted = false }
84
+ end
85
+
56
86
  private
57
87
 
88
+ # @rbs () -> bool
89
+ def another_live_process?
90
+ alive_after = SolidObjects.database_adapter.database_now -
91
+ SolidObjects.configuration.process_alive_threshold
92
+ Process
93
+ .where.not(shutdown_state: "stopped")
94
+ .where(last_heartbeat_at: alive_after..)
95
+ .where("hostname <> ? OR pid <> ?", Socket.gethostname, ::Process.pid)
96
+ .exists?
97
+ end
98
+
99
+ # @rbs () -> bool
100
+ def polling_warning_emitted?
101
+ @polling_warning_emitted
102
+ end
103
+
104
+ # @rbs () -> Mutex
105
+ def polling_warning_mutex
106
+ @polling_warning_mutex ||= Mutex.new
107
+ end
108
+
58
109
  # @rbs (Process, Time) -> void
59
110
  def cleanup_process(process_record, now)
60
111
  deregister(process_record, now:)
@@ -1,11 +1,14 @@
1
1
  # rbs_inline: enabled
2
2
 
3
+ require "solid_objects/polling_backoff"
4
+
3
5
  module SolidObjects
4
6
  class ReminderScheduler
5
7
  # @rbs @process_registry: ProcessRegistry
6
8
  # @rbs @database_adapter: DatabaseAdapter
7
9
  # @rbs @stopped: bool
8
10
  # @rbs @shutdown_requested: bool
11
+ # @rbs @polling_backoff: PollingBackoff
9
12
 
10
13
  # @rbs (?process_registry: ProcessRegistry, ?database_adapter: DatabaseAdapter) -> void
11
14
  def initialize(
@@ -17,6 +20,17 @@ module SolidObjects
17
20
  process_registry.register(kind: "reminder")
18
21
  @stopped = false
19
22
  @shutdown_requested = false
23
+ @polling_backoff = PollingBackoff.new(
24
+ minimum_interval: SolidObjects.configuration.polling_interval,
25
+ maximum_interval: SolidObjects.configuration.idle_polling_interval,
26
+ on_change: ->(transition) do
27
+ SolidObjects.instrument(
28
+ :"polling.interval_changed",
29
+ role: "reminders",
30
+ **transition
31
+ )
32
+ end
33
+ )
20
34
  end
21
35
 
22
36
  # @rbs (?now: Time?) -> bool
@@ -45,11 +59,23 @@ module SolidObjects
45
59
 
46
60
  # @rbs () -> void
47
61
  def run
62
+ ProcessRegistry.warn_if_polling_is_only_cross_process_wake_up
63
+
48
64
  until shutdown_requested?
65
+ wake_up = SolidObjects.wake_up
66
+ watch = wake_up.respond_to?(:watch) ? wake_up.watch : wake_up
49
67
  worked = run_once
50
- next if worked
51
-
52
- SolidObjects.wake_up.wait(timeout: SolidObjects.configuration.polling_interval)
68
+ if worked
69
+ polling_backoff.reset(:work)
70
+ next
71
+ end
72
+
73
+ notified = watch.wait(timeout: current_polling_interval)
74
+ if notified == false
75
+ polling_backoff.record_idle
76
+ else
77
+ polling_backoff.reset(:wake_up)
78
+ end
53
79
  end
54
80
  ensure
55
81
  stop
@@ -71,9 +97,14 @@ module SolidObjects
71
97
  @shutdown_requested
72
98
  end
73
99
 
100
+ # @rbs () -> Float
101
+ def current_polling_interval
102
+ polling_backoff.current_interval
103
+ end
104
+
74
105
  private
75
106
 
76
- attr_reader :process_registry, :database_adapter
107
+ attr_reader :process_registry, :database_adapter, :polling_backoff
77
108
 
78
109
  # @rbs (now: Time?) -> Reminder?
79
110
  def claim_next(now:)
@@ -1,5 +1,5 @@
1
1
  # rbs_inline: enabled
2
2
 
3
3
  module SolidObjects
4
- VERSION = "0.13.0"
4
+ VERSION = "0.13.2"
5
5
  end
@@ -2,23 +2,55 @@
2
2
 
3
3
  module SolidObjects
4
4
  class WakeUp
5
+ class Watch
6
+ # @rbs @wake_up: WakeUp
7
+ # @rbs @generation: Integer
8
+
9
+ # @rbs (WakeUp, Integer) -> void
10
+ def initialize(wake_up, generation)
11
+ @wake_up = wake_up
12
+ @generation = generation
13
+ end
14
+
15
+ # @rbs (timeout: Numeric) -> bool
16
+ def wait(timeout:)
17
+ @wake_up.wait(timeout:, generation: @generation)
18
+ end
19
+ end
20
+
5
21
  # @rbs @mutex: Mutex
6
22
  # @rbs @condition: Thread::ConditionVariable
23
+ # @rbs @generation: Integer
7
24
 
8
25
  # @rbs () -> void
9
26
  def initialize
10
27
  @mutex = Mutex.new
11
28
  @condition = Thread::ConditionVariable.new
29
+ @generation = 0
12
30
  end
13
31
 
14
32
  # @rbs () -> void
15
33
  def signal
16
- mutex.synchronize { condition.broadcast }
34
+ mutex.synchronize do
35
+ @generation += 1
36
+ condition.broadcast
37
+ end
17
38
  end
18
39
 
19
- # @rbs (timeout: Numeric) -> void
20
- def wait(timeout:)
21
- mutex.synchronize { condition.wait(mutex, timeout) }
40
+ # @rbs () -> Watch
41
+ def watch
42
+ mutex.synchronize { Watch.new(self, @generation) }
43
+ end
44
+
45
+ # @rbs (timeout: Numeric, ?generation: Integer?) -> bool
46
+ def wait(timeout:, generation: nil)
47
+ mutex.synchronize do
48
+ return true if generation && @generation != generation
49
+
50
+ generation ||= @generation
51
+ condition.wait(mutex, timeout)
52
+ @generation != generation
53
+ end
22
54
  end
23
55
 
24
56
  private
@@ -45,6 +45,12 @@ module SolidObjects
45
45
  false
46
46
  end
47
47
 
48
+ # @rbs () -> self
49
+ def watch
50
+ listen
51
+ self
52
+ end
53
+
48
54
  # Starts listening before a caller blocks, so a notification sent between
49
55
  # startup and the first wait is not missed.
50
56
  # @rbs () -> bool
@@ -12,6 +12,22 @@ module SolidObjects
12
12
  # polling interval remains the upper bound, so a missed or failed
13
13
  # notification costs latency rather than correctness.
14
14
  class Redis
15
+ class Watch
16
+ # @rbs @adapter: Redis
17
+ # @rbs @generation: Integer
18
+
19
+ # @rbs (Redis, Integer) -> void
20
+ def initialize(adapter, generation)
21
+ @adapter = adapter
22
+ @generation = generation
23
+ end
24
+
25
+ # @rbs (timeout: Numeric) -> bool
26
+ def wait(timeout:)
27
+ @adapter.wait(timeout:, generation: @generation)
28
+ end
29
+ end
30
+
15
31
  CHANNEL = "solid_objects_wake_up"
16
32
  FAILED_WAIT_INTERVAL = 0.05
17
33
  SUBSCRIBE_TIMEOUT = 5.0
@@ -52,9 +68,9 @@ module SolidObjects
52
68
  # The counter is snapshotted before subscribing, and re-checked before
53
69
  # blocking, so a signal delivered while this caller was still getting
54
70
  # ready is observed rather than absorbed into the new baseline.
55
- # @rbs (timeout: Numeric) -> bool
56
- def wait(timeout:)
57
- signalled = mutex.synchronize { @signalled }
71
+ # @rbs (timeout: Numeric, ?generation: Integer?) -> bool
72
+ def wait(timeout:, generation: nil)
73
+ signalled = generation || mutex.synchronize { @signalled }
58
74
  return paced_failure(timeout) unless listen
59
75
 
60
76
  mutex.synchronize do
@@ -65,6 +81,12 @@ module SolidObjects
65
81
  end
66
82
  end
67
83
 
84
+ # @rbs () -> Watch
85
+ def watch
86
+ listen
87
+ mutex.synchronize { Watch.new(self, @signalled) }
88
+ end
89
+
68
90
  # Redis delivers to a subscribed connection only, and a subscribed
69
91
  # connection cannot serve other callers, so one background subscription
70
92
  # per process fans out to every waiting role in memory. Subscribing
@@ -5,6 +5,7 @@ require "solid_objects/activation_manager"
5
5
  require "solid_objects/activation"
6
6
  require "solid_objects/executor"
7
7
  require "solid_objects/lease_renewer"
8
+ require "solid_objects/polling_backoff"
8
9
 
9
10
  module SolidObjects
10
11
  class Worker
@@ -13,6 +14,7 @@ module SolidObjects
13
14
  # @rbs @activations: Hash[Integer, Activation]
14
15
  # @rbs @stopped: bool
15
16
  # @rbs @shutdown_requested: bool
17
+ # @rbs @polling_backoff: PollingBackoff
16
18
 
17
19
  # @rbs (?process_registry: ProcessRegistry) -> void
18
20
  def initialize(process_registry: ProcessRegistry.new)
@@ -22,6 +24,23 @@ module SolidObjects
22
24
  @activations = {}
23
25
  @stopped = false
24
26
  @shutdown_requested = false
27
+ @polling_backoff = PollingBackoff.new(
28
+ minimum_interval: [
29
+ SolidObjects.configuration.polling_interval,
30
+ SolidObjects.configuration.lease_renewal_interval
31
+ ].min,
32
+ maximum_interval: [
33
+ SolidObjects.configuration.idle_polling_interval,
34
+ SolidObjects.configuration.lease_renewal_interval
35
+ ].min,
36
+ on_change: ->(transition) do
37
+ SolidObjects.instrument(
38
+ :"polling.interval_changed",
39
+ role: "actors",
40
+ **transition
41
+ )
42
+ end
43
+ )
25
44
  end
26
45
 
27
46
  # @rbs () -> Integer
@@ -70,11 +89,23 @@ module SolidObjects
70
89
 
71
90
  # @rbs () -> void
72
91
  def run
92
+ ProcessRegistry.warn_if_polling_is_only_cross_process_wake_up
93
+
73
94
  until shutdown_requested?
95
+ wake_up = SolidObjects.wake_up
96
+ watch = wake_up.respond_to?(:watch) ? wake_up.watch : wake_up
74
97
  processed = run_once
75
- next if processed.positive?
98
+ if processed.positive?
99
+ polling_backoff.reset(:work)
100
+ next
101
+ end
76
102
 
77
- SolidObjects.wake_up.wait(timeout: SolidObjects.configuration.polling_interval)
103
+ notified = watch.wait(timeout: current_polling_interval)
104
+ if notified == false
105
+ polling_backoff.record_idle
106
+ else
107
+ polling_backoff.reset(:wake_up)
108
+ end
78
109
  end
79
110
  ensure
80
111
  stop
@@ -107,9 +138,14 @@ module SolidObjects
107
138
  @shutdown_requested
108
139
  end
109
140
 
141
+ # @rbs () -> Float
142
+ def current_polling_interval
143
+ polling_backoff.current_interval
144
+ end
145
+
110
146
  private
111
147
 
112
- attr_reader :process_registry, :activation_manager, :activations
148
+ attr_reader :process_registry, :activation_manager, :activations, :polling_backoff
113
149
 
114
150
  # @rbs () -> Activation?
115
151
  def cached_ready_activation
data/lib/solid_objects.rb CHANGED
@@ -31,6 +31,7 @@ require "solid_objects/dead_letter_manager"
31
31
  require "solid_objects/message_pruner"
32
32
  require "solid_objects/instance_pruner"
33
33
  require "solid_objects/process_pruner"
34
+ require "solid_objects/administration"
34
35
  require "solid_objects/stream_name"
35
36
  require "solid_objects/dom_identity"
36
37
  require "solid_objects/stream_token"
@@ -52,6 +53,7 @@ require "solid_objects/wake_up"
52
53
  require "solid_objects/wake_up_adapters/postgresql"
53
54
  require "solid_objects/wake_up_adapters/redis"
54
55
  require "solid_objects/wake_up_adapters"
56
+ require "solid_objects/polling_backoff"
55
57
  require "solid_objects/effect_registry"
56
58
  require "solid_objects/commit_action_registry"
57
59
  require "solid_objects/lease"
@@ -136,6 +138,11 @@ module SolidObjects
136
138
  @dead_letters ||= DeadLetterManager.new
137
139
  end
138
140
 
141
+ # @rbs () -> Administration
142
+ def administration
143
+ @administration ||= Administration.new
144
+ end
145
+
139
146
  # @rbs (String | Symbol) -> String
140
147
  def table_name(name)
141
148
  "#{configuration.table_name_prefix}#{name}"
@@ -153,6 +160,7 @@ module SolidObjects
153
160
 
154
161
  # @rbs () -> void
155
162
  def reset!
163
+ ProcessRegistry.reset_polling_warning! if defined?(ProcessRegistry)
156
164
  @configuration = Configuration.new
157
165
  @registry = ActorRegistry.new
158
166
  @client = nil
@@ -162,6 +170,7 @@ module SolidObjects
162
170
  @effect_registry = EffectRegistry.new
163
171
  @commit_action_registry = CommitActionRegistry.new
164
172
  @dead_letters = nil
173
+ @administration = nil
165
174
  end
166
175
 
167
176
  # @rbs () -> DatabaseAdapter
@@ -32,9 +32,16 @@ module SolidObjects
32
32
  def members: () -> [ :name, :arguments ]
33
33
  end
34
34
 
35
+ # The reminders table holds a name in 191 characters.
36
+ REMINDER_NAME_LIMIT: ::Integer
37
+
38
+ REMINDER_KEY_SEPARATOR: ::String
39
+
35
40
  class ReminderIntent < Data
36
41
  attr_reader name(): untyped
37
42
 
43
+ attr_reader operation(): untyped
44
+
38
45
  attr_reader at(): untyped
39
46
 
40
47
  attr_reader arguments(): untyped
@@ -43,12 +50,12 @@ module SolidObjects
43
50
 
44
51
  attr_reader missed_policy(): untyped
45
52
 
46
- def self.new: (untyped name, untyped at, untyped arguments, untyped interval_seconds, untyped missed_policy) -> instance
47
- | (name: untyped, at: untyped, arguments: untyped, interval_seconds: untyped, missed_policy: untyped) -> instance
53
+ def self.new: (untyped name, untyped operation, untyped at, untyped arguments, untyped interval_seconds, untyped missed_policy) -> instance
54
+ | (name: untyped, operation: untyped, at: untyped, arguments: untyped, interval_seconds: untyped, missed_policy: untyped) -> instance
48
55
 
49
- def self.members: () -> [ :name, :at, :arguments, :interval_seconds, :missed_policy ]
56
+ def self.members: () -> [ :name, :operation, :at, :arguments, :interval_seconds, :missed_policy ]
50
57
 
51
- def members: () -> [ :name, :at, :arguments, :interval_seconds, :missed_policy ]
58
+ def members: () -> [ :name, :operation, :at, :arguments, :interval_seconds, :missed_policy ]
52
59
  end
53
60
 
54
61
  class OutboundMessageIntent < Data
@@ -157,8 +164,30 @@ module SolidObjects
157
164
  # @rbs (Symbol | String, **untyped) -> nil
158
165
  def commit_action: (Symbol | String, **untyped) -> nil
159
166
 
160
- # @rbs (at: Time, ?every: Numeric?, ?missed: Symbol | String) -> OperationDispatcher
161
- def schedule: (at: Time, ?every: Numeric?, ?missed: Symbol | String) -> OperationDispatcher
167
+ # A reminder is identified by its name, and without a key that name is the
168
+ # operation, so one actor holds one alarm per operation. A key gives an actor
169
+ # an alarm per item it is waiting on, which is what an actor holding a queue
170
+ # of scheduled work needs; the key is the caller's own identifier for the
171
+ # item, and scheduling the same key again moves that item's alarm.
172
+ # @rbs (at: Time, ?every: Numeric?, ?missed: Symbol | String, ?key: (String | Symbol | Integer)?) -> OperationDispatcher
173
+ def schedule: (at: Time, ?every: Numeric?, ?missed: Symbol | String, ?key: (String | Symbol | Integer)?) -> OperationDispatcher
174
+
175
+ # @rbs ((String | Symbol | Integer)?) -> String?
176
+ def validated_reminder_key: ((String | Symbol | Integer)?) -> String?
177
+
178
+ # A keyed name is the operation, a colon, and the key, so an operation
179
+ # holding a colon of its own would make two different schedules produce one
180
+ # name: an unkeyed "deliver:item" and a "deliver" keyed "item" would share a
181
+ # row, and the second would silently take the first one's alarm. Refusing a
182
+ # colon in the operation keeps unkeyed names free of colons, which leaves
183
+ # the two kinds of name disjoint and lets a key hold colons of its own.
184
+ #
185
+ # The length is checked on the composed name rather than the key alone,
186
+ # because a long operation and a short key can exceed the column just as
187
+ # easily as the reverse. Both are refused here rather than at the insert,
188
+ # once the turn is already doing work.
189
+ # @rbs (operation: Symbol | String, key: String?) -> String
190
+ def reminder_name: (operation: Symbol | String, key: String?) -> String
162
191
 
163
192
  # @rbs (Reference, ?available_at: Time?, ?idempotency_key: String?) -> OperationDispatcher
164
193
  def send_to: (Reference, ?available_at: Time?, ?idempotency_key: String?) -> OperationDispatcher
@@ -0,0 +1,13 @@
1
+ # Generated from lib/solid_objects/administration.rb with RBS::Inline
2
+
3
+ module SolidObjects
4
+ class Administration
5
+ # @rbs (?authorization_context: untyped) -> Array[Hash[Symbol, untyped]]
6
+ def processes: (?authorization_context: untyped) -> Array[Hash[Symbol, untyped]]
7
+
8
+ private
9
+
10
+ # @rbs (?authorization_context: untyped) -> void
11
+ def authorize!: (?authorization_context: untyped) -> void
12
+ end
13
+ end
@@ -2,6 +2,8 @@
2
2
 
3
3
  module SolidObjects
4
4
  class BroadcastExecutor
5
+ @polling_backoff: PollingBackoff
6
+
5
7
  @shutdown_requested: bool
6
8
 
7
9
  @stopped: bool
@@ -31,12 +33,17 @@ module SolidObjects
31
33
  # @rbs () -> bool
32
34
  def shutdown_requested?: () -> bool
33
35
 
36
+ # @rbs () -> Float
37
+ def current_polling_interval: () -> Float
38
+
34
39
  private
35
40
 
36
41
  attr_reader process_registry: untyped
37
42
 
38
43
  attr_reader database_adapter: untyped
39
44
 
45
+ attr_reader polling_backoff: untyped
46
+
40
47
  # @rbs () -> Broadcast?
41
48
  def claim_next: () -> Broadcast?
42
49