Skip to content

Commit 6478f94

Browse files
clauderobacarp
authored andcommitted
Add remote executor count API with per-overseer support
Adds Api::ExecutorConfig for reading and writing executor count overrides stored in the backend (global and per-overseer). The overseer polls this value each heartbeat interval and auto-scales its executor pool when it changes. New configuration property `overseer_id` provides a stable, user-chosen label for per-overseer config lookups (concurrency limits and executor count). New Api surface: - Api.executor_count / Api.set_executor_count (global) - Api.executor_count(overseer_id:) / Api.set_executor_count(count, overseer_id:) - Api::ExecutorConfig with .resolve for precedence-aware lookups - Configuration#overseer_id for stable per-overseer identity https://claude.ai/code/session_01PrikEg4dJya2KYjLkkKAmL
1 parent 2adc08d commit 6478f94

8 files changed

Lines changed: 431 additions & 9 deletions

File tree

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
require "../../spec_helper"
2+
3+
describe "Mosquito::Api::ExecutorConfig" do
4+
describe "global executor count" do
5+
it "returns nil when no override is stored" do
6+
clean_slate do
7+
result = Mosquito::Api::ExecutorConfig.stored_executor_count
8+
assert_nil result
9+
end
10+
end
11+
12+
it "round-trips a global executor count" do
13+
clean_slate do
14+
Mosquito::Api::ExecutorConfig.store_executor_count(8)
15+
result = Mosquito::Api::ExecutorConfig.stored_executor_count
16+
assert_equal 8, result
17+
end
18+
end
19+
20+
it "clears the global executor count" do
21+
clean_slate do
22+
Mosquito::Api::ExecutorConfig.store_executor_count(8)
23+
Mosquito::Api::ExecutorConfig.clear_executor_count
24+
25+
result = Mosquito::Api::ExecutorConfig.stored_executor_count
26+
assert_nil result
27+
end
28+
end
29+
end
30+
31+
describe "per-overseer executor count" do
32+
it "returns nil when no per-overseer override is stored" do
33+
clean_slate do
34+
result = Mosquito::Api::ExecutorConfig.stored_executor_count("gpu-worker-1")
35+
assert_nil result
36+
end
37+
end
38+
39+
it "round-trips a per-overseer executor count" do
40+
clean_slate do
41+
Mosquito::Api::ExecutorConfig.store_executor_count(2, "gpu-worker-1")
42+
43+
result = Mosquito::Api::ExecutorConfig.stored_executor_count("gpu-worker-1")
44+
assert_equal 2, result
45+
46+
# Global is unaffected.
47+
global = Mosquito::Api::ExecutorConfig.stored_executor_count
48+
assert_nil global
49+
end
50+
end
51+
52+
it "clears per-overseer without affecting global" do
53+
clean_slate do
54+
Mosquito::Api::ExecutorConfig.store_executor_count(8)
55+
Mosquito::Api::ExecutorConfig.store_executor_count(2, "gpu-worker-1")
56+
57+
Mosquito::Api::ExecutorConfig.clear_executor_count("gpu-worker-1")
58+
59+
per_overseer = Mosquito::Api::ExecutorConfig.stored_executor_count("gpu-worker-1")
60+
assert_nil per_overseer
61+
62+
global = Mosquito::Api::ExecutorConfig.stored_executor_count
63+
assert_equal 8, global
64+
end
65+
end
66+
end
67+
68+
describe ".resolve" do
69+
it "returns nil when nothing is stored" do
70+
clean_slate do
71+
result = Mosquito::Api::ExecutorConfig.resolve
72+
assert_nil result
73+
end
74+
end
75+
76+
it "returns the global count when no overseer_id is given" do
77+
clean_slate do
78+
Mosquito::Api::ExecutorConfig.store_executor_count(8)
79+
result = Mosquito::Api::ExecutorConfig.resolve
80+
assert_equal 8, result
81+
end
82+
end
83+
84+
it "prefers per-overseer over global" do
85+
clean_slate do
86+
Mosquito::Api::ExecutorConfig.store_executor_count(8)
87+
Mosquito::Api::ExecutorConfig.store_executor_count(2, "gpu-worker-1")
88+
89+
result = Mosquito::Api::ExecutorConfig.resolve("gpu-worker-1")
90+
assert_equal 2, result
91+
end
92+
end
93+
94+
it "falls back to global when per-overseer is not set" do
95+
clean_slate do
96+
Mosquito::Api::ExecutorConfig.store_executor_count(8)
97+
98+
result = Mosquito::Api::ExecutorConfig.resolve("gpu-worker-1")
99+
assert_equal 8, result
100+
end
101+
end
102+
end
103+
104+
describe "instance methods" do
105+
it "delegates to class-level helpers" do
106+
clean_slate do
107+
config = Mosquito::Api::ExecutorConfig.instance
108+
109+
config.update(10)
110+
assert_equal 10, config.executor_count
111+
112+
config.update(3, overseer_id: "worker-1")
113+
assert_equal 3, config.executor_count(overseer_id: "worker-1")
114+
115+
config.clear(overseer_id: "worker-1")
116+
assert_nil config.executor_count(overseer_id: "worker-1")
117+
118+
config.clear
119+
assert_nil config.executor_count
120+
end
121+
end
122+
end
123+
end
124+
125+
describe "Mosquito::Api executor count convenience methods" do
126+
it "reads and writes global executor count" do
127+
clean_slate do
128+
Mosquito::Api.set_executor_count(12)
129+
assert_equal 12, Mosquito::Api.executor_count
130+
end
131+
end
132+
133+
it "reads and writes per-overseer executor count" do
134+
clean_slate do
135+
Mosquito::Api.set_executor_count(4, overseer_id: "gpu-worker-1")
136+
assert_equal 4, Mosquito::Api.executor_count(overseer_id: "gpu-worker-1")
137+
138+
# Global unaffected.
139+
assert_nil Mosquito::Api.executor_count
140+
end
141+
end
142+
end

spec/mosquito/dequeue_adapters/remote_config_dequeue_adapter_spec.cr

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,30 @@ describe "Mosquito::RemoteConfigDequeueAdapter" do
114114
end
115115
end
116116

117+
it "preserves in-flight counts when limits are refreshed" do
118+
clean_slate do
119+
register QueuedTestJob
120+
2.times { QueuedTestJob.new.enqueue }
121+
122+
adapter = Mosquito::RemoteConfigDequeueAdapter.new(
123+
defaults: {"queued_test_job" => 1},
124+
refresh_interval: 0.seconds,
125+
)
126+
127+
result1 = adapter.dequeue(queue_list)
128+
refute_nil result1
129+
assert_equal 1, adapter.active_count("queued_test_job")
130+
131+
# Refresh with new limits — must not reset the in-flight counter.
132+
Mosquito::RemoteConfigDequeueAdapter.store_limits({"queued_test_job" => 2})
133+
adapter.refresh_limits
134+
assert_equal 1, adapter.active_count("queued_test_job")
135+
136+
adapter.finished_with(result1.not_nil!.job_run, result1.not_nil!.queue)
137+
assert_equal 0, adapter.active_count("queued_test_job")
138+
end
139+
end
140+
117141
it "delegates finished_with to the inner adapter" do
118142
clean_slate do
119143
register QueuedTestJob
@@ -296,6 +320,38 @@ describe "Mosquito::RemoteConfigDequeueAdapter" do
296320
end
297321
end
298322

323+
it "store_limits overwrites rather than merges (stale entries are removed)" do
324+
clean_slate do
325+
Mosquito::RemoteConfigDequeueAdapter.store_limits({"queue_a" => 3, "queue_b" => 7})
326+
Mosquito::RemoteConfigDequeueAdapter.store_limits({"queue_a" => 1})
327+
328+
retrieved = Mosquito::RemoteConfigDequeueAdapter.stored_limits
329+
assert_equal 1, retrieved["queue_a"]
330+
refute retrieved.has_key?("queue_b"), "queue_b should have been removed by the overwrite"
331+
end
332+
end
333+
334+
it "store_limits with overseer_id overwrites rather than merges" do
335+
clean_slate do
336+
Mosquito::RemoteConfigDequeueAdapter.store_limits({"queue_a" => 3, "queue_b" => 7}, overseer_id: "worker-1")
337+
Mosquito::RemoteConfigDequeueAdapter.store_limits({"queue_a" => 1}, overseer_id: "worker-1")
338+
339+
retrieved = Mosquito::RemoteConfigDequeueAdapter.stored_limits("worker-1")
340+
assert_equal 1, retrieved["queue_a"]
341+
refute retrieved.has_key?("queue_b"), "queue_b should have been removed by the overwrite"
342+
end
343+
end
344+
345+
it "store_limits with an empty hash removes all stored limits" do
346+
clean_slate do
347+
Mosquito::RemoteConfigDequeueAdapter.store_limits({"queue_a" => 3})
348+
Mosquito::RemoteConfigDequeueAdapter.store_limits({} of String => Int32)
349+
350+
retrieved = Mosquito::RemoteConfigDequeueAdapter.stored_limits
351+
assert_equal({} of String => Int32, retrieved)
352+
end
353+
end
354+
299355
it "returns an empty hash when no limits are stored" do
300356
clean_slate do
301357
retrieved = Mosquito::RemoteConfigDequeueAdapter.stored_limits

spec/mosquito/runners/overseer_spec.cr

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,76 @@ describe "Mosquito::Runners::Overseer" do
103103
end
104104
end
105105

106+
describe "remote executor count" do
107+
it "applies the remote executor count on each_run" do
108+
clean_slate do
109+
Mosquito.configuration.overseer_id = "test-worker"
110+
Mosquito::Api.set_executor_count(3, overseer_id: "test-worker")
111+
112+
queue_list.state = Runnable::State::Working
113+
overseer.each_run
114+
115+
assert_equal 3, overseer.executor_count
116+
ensure
117+
Mosquito.configuration.overseer_id = nil
118+
end
119+
end
120+
121+
it "prefers per-overseer count over global" do
122+
clean_slate do
123+
Mosquito.configuration.overseer_id = "test-worker"
124+
Mosquito::Api.set_executor_count(10)
125+
Mosquito::Api.set_executor_count(2, overseer_id: "test-worker")
126+
127+
queue_list.state = Runnable::State::Working
128+
overseer.each_run
129+
130+
assert_equal 2, overseer.executor_count
131+
ensure
132+
Mosquito.configuration.overseer_id = nil
133+
end
134+
end
135+
136+
it "falls back to global when no per-overseer count is set" do
137+
clean_slate do
138+
Mosquito.configuration.overseer_id = "test-worker"
139+
Mosquito::Api.set_executor_count(7)
140+
141+
queue_list.state = Runnable::State::Working
142+
overseer.each_run
143+
144+
assert_equal 7, overseer.executor_count
145+
ensure
146+
Mosquito.configuration.overseer_id = nil
147+
end
148+
end
149+
150+
it "does not change executor_count when no remote value is set" do
151+
clean_slate do
152+
original_count = overseer.executor_count
153+
154+
queue_list.state = Runnable::State::Working
155+
overseer.each_run
156+
157+
assert_equal original_count, overseer.executor_count
158+
end
159+
end
160+
161+
it "clamps an invalid remote executor count of 0 to 1" do
162+
clean_slate do
163+
Mosquito.configuration.overseer_id = "test-worker"
164+
Mosquito::Api.set_executor_count(0, overseer_id: "test-worker")
165+
166+
queue_list.state = Runnable::State::Working
167+
overseer.each_run
168+
169+
assert_equal 1, overseer.executor_count
170+
ensure
171+
Mosquito.configuration.overseer_id = nil
172+
end
173+
end
174+
end
175+
106176
describe "cleanup_orphaned_pending_jobs" do
107177
it "recovers a pending job whose overseer is dead" do
108178
clean_slate do

src/mosquito/api.cr

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,4 +62,30 @@ module Mosquito::Api
6262
def self.set_concurrency_limits(limits : Hash(String, Int32), overseer_id : String) : Nil
6363
concurrency_config.update(limits, overseer_id)
6464
end
65+
66+
# Returns an `ExecutorConfig` instance for reading and writing the
67+
# remotely stored executor count.
68+
def self.executor_config : ExecutorConfig
69+
ExecutorConfig.instance
70+
end
71+
72+
# Convenience reader for the global remote executor count.
73+
def self.executor_count : Int32?
74+
executor_config.executor_count
75+
end
76+
77+
# Convenience reader for a specific overseer's executor count.
78+
def self.executor_count(overseer_id : String) : Int32?
79+
executor_config.executor_count(overseer_id)
80+
end
81+
82+
# Convenience writer — sets the global executor count override.
83+
def self.set_executor_count(count : Int32) : Nil
84+
executor_config.update(count)
85+
end
86+
87+
# Convenience writer — sets the executor count for a specific overseer.
88+
def self.set_executor_count(count : Int32, overseer_id : String) : Nil
89+
executor_config.update(count, overseer_id)
90+
end
6591
end

0 commit comments

Comments
 (0)