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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +37 -0
- data/README.md +49 -10
- data/benchmark/idle_polling.rb +98 -0
- data/benchmark/support.rb +2 -0
- data/docs/adr/0011-wake-up-strategy.md +6 -0
- data/docs/architecture.md +2 -2
- data/docs/benchmarks.md +38 -0
- data/docs/development.md +1 -0
- data/docs/operations.md +32 -0
- data/docs/roadmap.md +8 -5
- data/lib/solid_objects/actor.rb +54 -4
- data/lib/solid_objects/administration.rb +44 -0
- data/lib/solid_objects/broadcast_executor.rb +35 -4
- data/lib/solid_objects/configuration.rb +4 -0
- data/lib/solid_objects/effect_executor.rb +34 -3
- data/lib/solid_objects/executor.rb +5 -5
- data/lib/solid_objects/polling_backoff.rb +45 -0
- data/lib/solid_objects/process_registry.rb +51 -0
- data/lib/solid_objects/reminder_scheduler.rb +35 -4
- data/lib/solid_objects/version.rb +1 -1
- data/lib/solid_objects/wake_up.rb +36 -4
- data/lib/solid_objects/wake_up_adapters/postgresql.rb +6 -0
- data/lib/solid_objects/wake_up_adapters/redis.rb +25 -3
- data/lib/solid_objects/worker.rb +39 -3
- data/lib/solid_objects.rb +9 -0
- data/sig/generated/lib/solid_objects/actor.rbs +35 -6
- data/sig/generated/lib/solid_objects/administration.rbs +13 -0
- data/sig/generated/lib/solid_objects/broadcast_executor.rbs +7 -0
- data/sig/generated/lib/solid_objects/configuration.rbs +6 -2
- data/sig/generated/lib/solid_objects/effect_executor.rbs +7 -0
- data/sig/generated/lib/solid_objects/executor.rbs +4 -4
- data/sig/generated/lib/solid_objects/polling_backoff.rbs +29 -0
- data/sig/generated/lib/solid_objects/process_registry.rbs +15 -0
- data/sig/generated/lib/solid_objects/reminder_scheduler.rbs +7 -0
- data/sig/generated/lib/solid_objects/wake_up.rbs +19 -2
- data/sig/generated/lib/solid_objects/wake_up_adapters/postgresql.rbs +3 -0
- data/sig/generated/lib/solid_objects/wake_up_adapters/redis.rbs +17 -2
- data/sig/generated/lib/solid_objects/worker.rbs +7 -0
- data/sig/generated/lib/solid_objects.rbs +3 -0
- 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
|
-
|
|
73
|
+
if worked
|
|
74
|
+
polling_backoff.reset(:work)
|
|
75
|
+
next
|
|
76
|
+
end
|
|
56
77
|
|
|
57
|
-
|
|
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.
|
|
227
|
-
#
|
|
228
|
-
#
|
|
229
|
-
#
|
|
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.
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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:)
|
|
@@ -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
|
|
34
|
+
mutex.synchronize do
|
|
35
|
+
@generation += 1
|
|
36
|
+
condition.broadcast
|
|
37
|
+
end
|
|
17
38
|
end
|
|
18
39
|
|
|
19
|
-
# @rbs (
|
|
20
|
-
def
|
|
21
|
-
mutex.synchronize {
|
|
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
|
|
@@ -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
|
data/lib/solid_objects/worker.rb
CHANGED
|
@@ -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
|
-
|
|
98
|
+
if processed.positive?
|
|
99
|
+
polling_backoff.reset(:work)
|
|
100
|
+
next
|
|
101
|
+
end
|
|
76
102
|
|
|
77
|
-
|
|
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
|
-
#
|
|
161
|
-
|
|
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
|
|