Skip to content

Commit abdc026

Browse files
committed
Adds the ability for an overseer to modify it's executor pool size.
When the executor_count is incorrect, it'll now add or remove executors to correct the pool size.
1 parent e2b2607 commit abdc026

5 files changed

Lines changed: 79 additions & 30 deletions

File tree

spec/helpers/mock_executor.cr

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
class MockExecutor < Mosquito::Runners::Executor
2-
setter job_run : Mosquito::JobRun?
3-
setter queue : Mosquito::Queue?
2+
setter work_unit : Mosquito::WorkUnit?
43

54
def state=(state : Mosquito::Runnable::State)
65
super

spec/mosquito/runners/executor_spec.cr

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,7 @@ describe "Mosquito::Runners::Executor" do
1616
register job_class
1717
job_class.reset_performance_counter!
1818
job_run = job_class.new.enqueue
19-
executor.job_run = job_run
20-
executor.queue = job_class.queue
19+
executor.work_unit = WorkUnit.of(job_run, from: job_class.queue)
2120
executor.execute
2221
end
2322

@@ -62,8 +61,7 @@ describe "Mosquito::Runners::Executor" do
6261
FailingJob.queue.enqueue job_run
6362

6463
Timecop.freeze now do
65-
executor.job_run = job_run
66-
executor.queue = FailingJob.queue
64+
executor.work_unit = WorkUnit.of(job_run, from: FailingJob.queue)
6765
executor.execute
6866
end
6967

@@ -72,8 +70,7 @@ describe "Mosquito::Runners::Executor" do
7270

7371
Timecop.freeze now + job.reschedule_interval(1) do
7472
coordinator.enqueue_delayed_jobs
75-
executor.job_run = job_run
76-
executor.queue = FailingJob.queue
73+
executor.work_unit = WorkUnit.of(job_run, from: FailingJob.queue)
7774
executor.execute
7875
end
7976

@@ -91,8 +88,7 @@ describe "Mosquito::Runners::Executor" do
9188
job_run.store
9289
NonReschedulableFailingJob.queue.enqueue job_run
9390

94-
executor.job_run = job_run
95-
executor.queue = NonReschedulableFailingJob.queue
91+
executor.work_unit = WorkUnit.of(job_run, from: NonReschedulableFailingJob.queue)
9692
executor.execute
9793

9894
actual_ttl = backend.expires_in job_run.config_key
@@ -109,8 +105,7 @@ describe "Mosquito::Runners::Executor" do
109105
job_run.store
110106
QueuedTestJob.queue.enqueue job_run
111107

112-
executor.job_run = job_run
113-
executor.queue = QueuedTestJob.queue
108+
executor.work_unit = WorkUnit.of(job_run, from: QueuedTestJob.queue)
114109
executor.execute
115110

116111
assert_logs_match "Success"
@@ -143,8 +138,7 @@ describe "Mosquito::Runners::Executor" do
143138
api
144139

145140
spawn {
146-
executor.job_run = job_run
147-
executor.queue = SleepyJob.queue
141+
executor.work_unit = WorkUnit.of(job_run, from: SleepyJob.queue)
148142
executor.execute
149143
job_finished.send true
150144
}

src/mosquito/runner.cr

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,10 @@ module Mosquito
7171
end
7272
end
7373

74+
def self.overseer
75+
instance.overseer
76+
end
77+
7478
private def self.instance : self
7579
@@instance ||= new
7680
end

src/mosquito/runners/executor.cr

Lines changed: 51 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,7 @@ module Mosquito::Runners
3232

3333
# Where work is received from the overseer.
3434
getter job_pipeline : Channel(WorkUnit)
35-
getter! job_run : JobRun
36-
getter! queue : Queue
35+
getter! work_unit : WorkUnit
3736

3837
# Used to notify the overseer when this executor is idle.
3938
# Sends the {JobRun, Queue} tuple that was just finished, or nil
@@ -45,14 +44,30 @@ module Mosquito::Runners
4544
Observability::Executor.new self
4645
}
4746

47+
getter? decommissioned : Bool = false
48+
@stop_channel = Channel(Nil).new(1)
49+
50+
# Marks this executor for graceful shutdown. It will stop after
51+
# completing its current job (if any).
52+
def decommission!
53+
return if @decommissioned
54+
@decommissioned = true
55+
@stop_channel.send(nil)
56+
end
57+
58+
private def job_run : JobRun
59+
work_unit.job_run
60+
end
61+
62+
private def queue : Queue
63+
work_unit.queue
64+
end
65+
4866
private def state=(state : State)
4967
# Send a message to the overseer that this executor is idle,
5068
# including the job that was just finished (if any).
5169
if state == State::Idle
52-
finished = if (jr = @job_run) && (q = @queue)
53-
WorkUnit.of(jr, from: q)
54-
end
55-
spawn { finished_bell.send finished }
70+
spawn { finished_bell.send @work_unit }
5671
end
5772

5873
super
@@ -76,19 +91,44 @@ module Mosquito::Runners
7691
self.state = State::Idle
7792
end
7893

94+
def stop(wait_group : WaitGroup = WaitGroup.new(1)) : WaitGroup
95+
decommission!
96+
super
97+
end
98+
7999
# :nodoc:
80100
def each_run : Nil
81-
dequeue = job_pipeline.receive?
82-
return if dequeue.nil?
101+
if @decommissioned
102+
self.state = State::Stopping
103+
return
104+
end
105+
106+
dequeue : WorkUnit? = nil
107+
begin
108+
select
109+
when dequeue = job_pipeline.receive
110+
when @stop_channel.receive
111+
self.state = State::Stopping
112+
return
113+
end
114+
rescue Channel::ClosedError
115+
return
116+
end
117+
118+
return unless dequeue
83119

84120
self.state = State::Working
85-
@job_run = dequeue.job_run
86-
@queue = dequeue.queue
121+
@work_unit = dequeue
87122
log.trace { "Dequeued #{job_run} from #{queue.name}" }
88123
execute
89124
log.trace { "Finished #{job_run} from #{queue.name}" }
90-
self.state = State::Idle
91125

126+
if @decommissioned
127+
self.state = State::Stopping
128+
return
129+
end
130+
131+
self.state = State::Idle
92132
observer.heartbeat!
93133
end
94134

src/mosquito/runners/overseer.cr

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ module Mosquito::Runners
3434
# The number of executors to start.
3535
getter executor_count : Int32
3636

37+
def executor_count=(count : Int32)
38+
@executor_count = Math.max(count, 1)
39+
end
40+
3741
getter idle_wait : Time::Span
3842

3943
def initialize
@@ -165,7 +169,7 @@ module Mosquito::Runners
165169
spawn { @finished_notifier.send nil }
166170
end
167171

168-
check_for_deceased_runners
172+
adjust_executor_pool
169173

170174
run_at_most every: Mosquito.configuration.heartbeat_interval, label: :heartbeat do
171175
observer.heartbeat
@@ -195,18 +199,26 @@ module Mosquito::Runners
195199
#
196200
# When a dead executor is found, any job it was working on has its
197201
# failure counter incremented and follows the standard retry logic.
198-
def check_for_deceased_runners : Nil
202+
def adjust_executor_pool : Nil
203+
# Remove dead/crashed executors and recover their jobs.
199204
executors.select {|executor| executor.dead? || executor.state.crashed? }
200205
.each do |dead_executor|
201206
observer.executor_died dead_executor
202207
recover_job_from dead_executor
203208
executors.delete dead_executor
204209
end
205210

211+
# Scale up: spawn new executors to reach the target count.
206212
(executor_count - executors.size).times do
207213
executors << build_executor.tap(&.run)
208214
end
209215

216+
# Scale down: decommission excess executors and remove them from the pool.
217+
# They will finish their current job (if any) and then stop.
218+
while executors.size > executor_count
219+
executors.pop.decommission!
220+
end
221+
210222
observer.update_executor_list executors
211223

212224
if queue_list.dead?
@@ -274,10 +286,10 @@ module Mosquito::Runners
274286
# If a dead executor was working on a job, increment its failure
275287
# counter and follow the standard retry logic.
276288
private def recover_job_from(dead_executor : Executor) : Nil
277-
return unless job_run = dead_executor.job_run?
289+
return unless work_unit = dead_executor.work_unit?
278290

279-
observer.recovered_job_from_executor job_run, dead_executor
280-
job_run.retry_or_banish dead_executor.queue
291+
observer.recovered_job_from_executor work_unit.job_run, dead_executor
292+
work_unit.job_run.retry_or_banish work_unit.queue
281293
end
282294

283295
end

0 commit comments

Comments
 (0)