Skip to content

Commit dbe66cc

Browse files
committed
feat: expose process administration
Provide an authorization-guarded process query through the runtime database adapter so operators can inspect live and stale rows without opening a competing SQLite connection. Keep the polling warning evidence-based and document rolling-deployment overlap.
1 parent 00415eb commit dbe66cc

10 files changed

Lines changed: 134 additions & 3 deletions

File tree

CHANGELOG.md

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

3+
## 0.13.2 - 2026-08-16
4+
5+
- Add an authorized `SolidObjects.administration.processes` query for
6+
inspecting live and stale process rows through the runtime database adapter.
7+
- Document rolling-deployment overlap as a reason for the polling-only warning.
8+
39
## 0.13.1 - 2026-08-16
410

511
- Back idle actor, effect, reminder, and broadcast polling off exponentially

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.13.1)
4+
solid_objects (0.13.2)
55
actioncable (>= 8.0)
66
actionpack (>= 8.0)
77
actionview (>= 8.0)
@@ -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.13.1)
387+
solid_objects (0.13.2)
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

docs/operations.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,14 @@ adapter is configured, the runtime logs
9898
need prompt delivery. Without one, newly committed work can wait up to the
9999
current idle polling interval.
100100

101+
The warning excludes process rows with the current hostname and PID. It can
102+
therefore appear during a rolling deployment or restart overlap when an older
103+
and newer process briefly share the database. A process that stopped without
104+
graceful cleanup remains live until its heartbeat exceeds
105+
`process_alive_threshold`; inspect `SolidObjects.administration.processes` to
106+
distinguish a live overlap from a stale row without opening a second SQLite
107+
connection.
108+
101109
Each role exposes `current_polling_interval`.
102110
`solid_objects.polling.interval_changed` reports the role, reason, previous
103111
interval, and current interval. The polling-only warning is also emitted as

lib/solid_objects.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
require "solid_objects/message_pruner"
3232
require "solid_objects/instance_pruner"
3333
require "solid_objects/process_pruner"
34+
require "solid_objects/administration"
3435
require "solid_objects/stream_name"
3536
require "solid_objects/dom_identity"
3637
require "solid_objects/stream_token"
@@ -137,6 +138,11 @@ def dead_letters
137138
@dead_letters ||= DeadLetterManager.new
138139
end
139140

141+
# @rbs () -> Administration
142+
def administration
143+
@administration ||= Administration.new
144+
end
145+
140146
# @rbs (String | Symbol) -> String
141147
def table_name(name)
142148
"#{configuration.table_name_prefix}#{name}"
@@ -164,6 +170,7 @@ def reset!
164170
@effect_registry = EffectRegistry.new
165171
@commit_action_registry = CommitActionRegistry.new
166172
@dead_letters = nil
173+
@administration = nil
167174
end
168175

169176
# @rbs () -> DatabaseAdapter
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# rbs_inline: enabled
2+
3+
module SolidObjects
4+
class Administration
5+
# @rbs (?authorization_context: untyped) -> Array[Hash[Symbol, untyped]]
6+
def processes(authorization_context: nil)
7+
authorize!(authorization_context:)
8+
now = SolidObjects.database_adapter.database_now
9+
stale_at = now - SolidObjects.configuration.process_alive_threshold
10+
11+
Process.order(:kind, :started_at).map do |process_record|
12+
{
13+
id: process_record.id,
14+
kind: process_record.kind,
15+
hostname: process_record.hostname,
16+
pid: process_record.pid,
17+
metadata: process_record.metadata,
18+
shutdown_state: process_record.shutdown_state,
19+
shutdown_requested_at: process_record.shutdown_requested_at,
20+
started_at: process_record.started_at,
21+
last_heartbeat_at: process_record.last_heartbeat_at,
22+
stopped_at: process_record.stopped_at,
23+
stale: process_record.shutdown_state != "stopped" &&
24+
process_record.last_heartbeat_at <= stale_at
25+
}.freeze
26+
end.freeze
27+
end
28+
29+
private
30+
31+
# @rbs (?authorization_context: untyped) -> void
32+
def authorize!(authorization_context: nil)
33+
authorized = SolidObjects.configuration.authorize_administration.call(
34+
action: :inspect,
35+
resource: "processes",
36+
resource_id: nil,
37+
authorization_context:
38+
)
39+
return if authorized
40+
41+
raise Unauthorized, "actor administration is not authorized"
42+
end
43+
end
44+
end

lib/solid_objects/version.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# rbs_inline: enabled
22

33
module SolidObjects
4-
VERSION = "0.13.1"
4+
VERSION = "0.13.2"
55
end

sig/generated/lib/solid_objects.rbs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ module SolidObjects
3636
# @rbs () -> DeadLetterManager
3737
def self.dead_letters: () -> DeadLetterManager
3838

39+
# @rbs () -> Administration
40+
def self.administration: () -> Administration
41+
3942
# @rbs (String | Symbol) -> String
4043
def self.table_name: (String | Symbol) -> String
4144

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# frozen_string_literal: true
2+
3+
require "database_test_helper"
4+
5+
class AdministrationTest < ActiveSupport::TestCase
6+
test "requires administration authorization for process inspection" do
7+
assert_raises(SolidObjects::Unauthorized) do
8+
SolidObjects.administration.processes
9+
end
10+
end
11+
12+
test "returns frozen process snapshots with current liveness" do
13+
SolidObjects.configuration.authorize_administration = ->(**) { true }
14+
process = SolidObjects::Process.create!(
15+
id: SecureRandom.uuid,
16+
kind: "worker",
17+
hostname: "test-host",
18+
pid: ::Process.pid,
19+
started_at: Time.current,
20+
last_heartbeat_at: Time.current,
21+
metadata: { "solid_objects_version" => SolidObjects::VERSION }
22+
)
23+
24+
processes = SolidObjects.administration.processes
25+
26+
snapshot = processes.find { |record| record[:id] == process.id }
27+
assert_equal "worker", snapshot[:kind]
28+
assert_equal false, snapshot[:stale]
29+
assert_predicate processes, :frozen?
30+
assert_predicate snapshot, :frozen?
31+
end
32+
end

test/integration/polling_test.rb

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,24 @@ def signal
251251
workers&.each(&:stop)
252252
end
253253

254+
test "does not warn when all observed processes share the current Ruby process" do
255+
logger = RecordingLogger.new
256+
SolidObjects.configuration.logger = logger
257+
SolidObjects::Process.create!(
258+
id: SecureRandom.uuid,
259+
kind: "worker",
260+
hostname: Socket.gethostname,
261+
pid: ::Process.pid,
262+
started_at: Time.current,
263+
last_heartbeat_at: Time.current,
264+
metadata: {}
265+
)
266+
267+
SolidObjects::ProcessRegistry.warn_if_polling_is_only_cross_process_wake_up
268+
269+
assert_empty logger.warnings
270+
end
271+
254272
test "does not warn when a cross-process wake-up adapter is configured" do
255273
logger = RecordingLogger.new
256274
SolidObjects.configuration.logger = logger

0 commit comments

Comments
 (0)