feat(engine): Scheduler fiber publishes SchedulerTick on a fixed interval

Initial tick fires immediately on start so first policy evaluation
doesn't wait for the full interval on boot. The PolicyEvaluator
subscribes to SchedulerTick events and invokes evaluate_all -> overdue
discovery -> RotationScheduled fan-out. 3 specs verify boot-tick,
periodic cadence, and clean stop().
This commit is contained in:
CarterPerez-dev 2026-04-29 01:05:44 -04:00
parent 873257ad4b
commit d7d5f10825
2 changed files with 128 additions and 0 deletions

View File

@ -0,0 +1,76 @@
# ===================
# ©AngelaMos | 2026
# scheduler_spec.cr
# ===================
require "../../spec_helper"
require "../../../src/cre/engine/scheduler"
require "../../../src/cre/engine/event_bus"
private def drain(ch : ::Channel(CRE::Events::Event)) : Array(CRE::Events::Event)
out = [] of CRE::Events::Event
loop do
select
when ev = ch.receive
out << ev
else
break
end
end
out
end
describe CRE::Engine::Scheduler do
it "publishes a tick immediately on start" do
bus = CRE::Engine::EventBus.new
ch = bus.subscribe
bus.run
scheduler = CRE::Engine::Scheduler.new(bus, interval: 5.seconds)
scheduler.start
sleep 0.1.seconds
scheduler.stop
ticks = drain(ch).count(&.is_a?(CRE::Events::SchedulerTick))
ticks.should be >= 1
ensure
bus.try(&.stop)
end
it "publishes ticks at the configured interval" do
bus = CRE::Engine::EventBus.new
ch = bus.subscribe(buffer: 64)
bus.run
scheduler = CRE::Engine::Scheduler.new(bus, interval: 0.05.seconds)
scheduler.start
sleep 0.18.seconds
scheduler.stop
sleep 0.05.seconds # let final tick land
ticks = drain(ch).count(&.is_a?(CRE::Events::SchedulerTick))
# Initial + ~3 interval ticks = 3-5 expected; allow some scheduling variance
ticks.should be >= 2
ticks.should be <= 6
ensure
bus.try(&.stop)
end
it "stop halts publication" do
bus = CRE::Engine::EventBus.new
ch = bus.subscribe
bus.run
scheduler = CRE::Engine::Scheduler.new(bus, interval: 0.05.seconds)
scheduler.start
sleep 0.06.seconds
scheduler.stop
drain(ch) # drain whatever was already published
sleep 0.2.seconds
later = drain(ch).count(&.is_a?(CRE::Events::SchedulerTick))
later.should be <= 1 # at most one in-flight tick from the last sleep cycle
ensure
bus.try(&.stop)
end
end

View File

@ -0,0 +1,52 @@
# ===================
# ©AngelaMos | 2026
# scheduler.cr
# ===================
require "log"
require "./event_bus"
require "../events/system_events"
module CRE::Engine
# Scheduler is a fiber that publishes SchedulerTick events at a fixed interval.
# The PolicyEvaluator subscriber listens for these and runs evaluate_all,
# which discovers overdue credentials and publishes RotationScheduled etc.
#
# The fiber owns its own lifecycle: start() spawns it; stop() flips a flag and
# the next tick's check exits cleanly.
class Scheduler
Log = ::Log.for("cre.scheduler")
@running : Bool
def initialize(@bus : EventBus, @interval : Time::Span = 60.seconds)
@running = false
end
def start : Nil
@running = true
spawn(name: "scheduler") do
# Fire one tick immediately so the first evaluation doesn't wait
# for the full interval on boot.
@bus.publish Events::SchedulerTick.new
while @running
sleep @interval
break unless @running
begin
@bus.publish Events::SchedulerTick.new
rescue ex
Log.error(exception: ex) { "scheduler tick publish failed" }
end
end
end
end
def stop : Nil
@running = false
end
def running? : Bool
@running
end
end
end