Skip to content

Commit 33cf434

Browse files
clauderobacarp
authored andcommitted
Add Api::PeriodicJob to query periodic job history and broadcast execution events
Expose periodic job last-execution timestamps through the public API via Api::PeriodicJob and Api.list_periodic_jobs. Add Observability::PeriodicJob to broadcast events when periodic jobs are enqueued or skipped. https://claude.ai/code/session_01QNAYckqy4eKc6EvTdDNLU7
1 parent 6d7edd2 commit 33cf434

4 files changed

Lines changed: 136 additions & 0 deletions

File tree

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
require "../../spec_helper"
2+
3+
describe Mosquito::Api::PeriodicJob do
4+
getter interval : Time::Span = 2.minutes
5+
6+
describe "publish context" do
7+
it "includes the periodic job name" do
8+
clean_slate do
9+
Mosquito::Base.register_job_interval PeriodicTestJob, interval: interval
10+
job_run = Mosquito::Base.scheduled_job_runs.first
11+
observer = job_run.observer
12+
assert_equal "periodic_job:PeriodicTestJob", observer.publish_context.context
13+
assert_equal "mosquito:periodic_job:PeriodicTestJob", observer.publish_context.originator
14+
end
15+
end
16+
end
17+
18+
it "can fetch a list of periodic jobs" do
19+
clean_slate do
20+
Mosquito::Base.register_job_interval PeriodicTestJob, interval: interval
21+
periodic_jobs = Mosquito::Api::PeriodicJob.all
22+
assert_equal 1, periodic_jobs.size
23+
assert_equal "PeriodicTestJob", periodic_jobs.first.name
24+
assert_equal interval, periodic_jobs.first.interval
25+
end
26+
end
27+
28+
it "returns nil for last_executed_at when never run" do
29+
clean_slate do
30+
Mosquito::Base.register_job_interval PeriodicTestJob, interval: interval
31+
periodic_jobs = Mosquito::Api::PeriodicJob.all
32+
assert_nil periodic_jobs.first.last_executed_at
33+
end
34+
end
35+
36+
it "returns the last executed time after a job runs" do
37+
now = Time.utc.at_beginning_of_second
38+
clean_slate do
39+
Mosquito::Base.register_job_interval PeriodicTestJob, interval: interval
40+
job_run = Mosquito::Base.scheduled_job_runs.first
41+
42+
Timecop.freeze(now) do
43+
job_run.try_to_execute
44+
end
45+
46+
periodic_jobs = Mosquito::Api::PeriodicJob.all
47+
assert_equal now, periodic_jobs.first.last_executed_at
48+
end
49+
end
50+
51+
it "publishes an event when a periodic job is enqueued" do
52+
now = Time.utc.at_beginning_of_second
53+
clean_slate do
54+
Mosquito::Base.register_job_interval PeriodicTestJob, interval: interval
55+
56+
eavesdrop do
57+
Timecop.freeze(now) do
58+
Mosquito::Base.scheduled_job_runs.first.try_to_execute
59+
end
60+
end
61+
62+
assert_message_received /enqueued/
63+
end
64+
end
65+
end

src/mosquito/api.cr

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ module Mosquito::Api
1515
JobRun.new id
1616
end
1717

18+
def self.list_periodic_jobs : Array(PeriodicJob)
19+
PeriodicJob.all
20+
end
21+
1822
def self.list_queues : Array(Observability::Queue)
1923
Mosquito.backend.list_queues
2024
.map { |name| Observability::Queue.new name }

src/mosquito/api/periodic_job.cr

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
module Mosquito
2+
# An interface for inspecting the state of periodic jobs.
3+
#
4+
# This class provides read-only access to periodic job metadata,
5+
# including the last time each periodic job was executed.
6+
#
7+
# ```
8+
# Mosquito::Api::PeriodicJob.all.each do |job|
9+
# puts "#{job.name} last ran at #{job.last_executed_at}"
10+
# end
11+
# ```
12+
class Api::PeriodicJob
13+
# The name of the periodic job class.
14+
getter name : String
15+
16+
# The configured run interval for this periodic job.
17+
getter interval : Time::Span | Time::MonthSpan
18+
19+
private getter metadata : Metadata
20+
21+
# Returns a list of all registered periodic jobs.
22+
def self.all : Array(self)
23+
Base.scheduled_job_runs.map do |job_run|
24+
new job_run.class.name, job_run.interval
25+
end
26+
end
27+
28+
def initialize(@name : String, @interval : Time::Span | Time::MonthSpan)
29+
@metadata = Metadata.new(
30+
Mosquito.backend.build_key("periodic_jobs", @name),
31+
readonly: true
32+
)
33+
end
34+
35+
# The last time this periodic job was executed, or nil if it has never run.
36+
def last_executed_at : Time?
37+
if timestamp = metadata["last_executed_at"]?
38+
Time.unix(timestamp.to_i)
39+
end
40+
end
41+
end
42+
43+
class Observability::PeriodicJob
44+
include Publisher
45+
46+
getter log : ::Log
47+
getter publish_context : PublishContext
48+
49+
def initialize(periodic_job_run : Mosquito::PeriodicJobRun)
50+
@name = periodic_job_run.class.name
51+
@publish_context = PublishContext.new [:periodic_job, @name]
52+
@log = Log.for(@name)
53+
end
54+
55+
def enqueued(at time : Time)
56+
log.info { "Enqueued periodic job at #{time}" }
57+
publish({event: "enqueued", executed_at: time.to_unix})
58+
end
59+
60+
def skipped
61+
log.trace { "Not yet due for execution" }
62+
end
63+
end
64+
end

src/mosquito/periodic_job_run.cr

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ module Mosquito
55
property class : Mosquito::PeriodicJob.class
66
property interval : Time::Span | Time::MonthSpan
77
getter metadata : Metadata { Metadata.new(Mosquito.backend.build_key("periodic_jobs", @class.name)) }
8+
getter observer : Observability::PeriodicJob { Observability::PeriodicJob.new(self) }
89

910
# The last executed timestamp for this periodicjob tracked by the backend.
1011
def last_executed_at?
@@ -56,8 +57,10 @@ module Mosquito
5657
end
5758

5859
self.last_executed_at = now
60+
observer.enqueued(at: now)
5961
true
6062
else
63+
observer.skipped
6164
false
6265
end
6366
end

0 commit comments

Comments
 (0)