Class: Clacky::Server::Scheduler

Inherits:
Object
  • Object
show all
Defined in:
lib/clacky/server/scheduler.rb

Overview

Scheduler reads ~/.clacky/schedules.yml and runs tasks on a cron-like schedule.

It starts a background thread that ticks every 60 seconds, checks all configured schedules, and fires any task whose cron expression matches the current time.

Schedule file format (~/.clacky/schedules.yml):

- name: daily_report
  task: daily_report          # references ~/.clacky/tasks/daily_report.md
  cron: "0 9 * * 1-5"        # standard 5-field cron expression
  enabled: true               # optional, defaults to true

Cron field order: minute hour day-of-month month day-of-week

Constant Summary collapse

SCHEDULES_FILE =
File.expand_path("~/.clacky/schedules.yml")
TASKS_DIR =
File.expand_path("~/.clacky/tasks")

Instance Method Summary collapse

Constructor Details

#initialize(session_registry:, session_builder:) ⇒ Scheduler

Returns a new instance of Scheduler.



26
27
28
29
30
31
32
# File 'lib/clacky/server/scheduler.rb', line 26

def initialize(session_registry:, session_builder:)
  @registry        = session_registry
  @session_builder = session_builder  # callable: (name:, working_dir:) -> session_id
  @thread          = nil
  @running         = false
  @mutex           = Mutex.new
end

Instance Method Details

#add_schedule(name:, task:, cron:, enabled: true) ⇒ Object

Add or update a schedule entry in schedules.yml.



66
67
68
69
70
71
72
73
74
75
76
77
# File 'lib/clacky/server/scheduler.rb', line 66

def add_schedule(name:, task:, cron:, enabled: true)
  list = load_schedules
  # Remove existing entry with the same name
  list.reject! { |s| s["name"] == name }
  list << {
    "name"    => name,
    "task"    => task,
    "cron"    => cron,
    "enabled" => enabled
  }
  save_schedules(list)
end

#create_cron_task(name:, content:, cron:, enabled: true) ⇒ Object

Create a task file and its schedule in one step.



104
105
106
107
# File 'lib/clacky/server/scheduler.rb', line 104

def create_cron_task(name:, content:, cron:, enabled: true)
  write_task(name, content)
  add_schedule(name: name, task: name, cron: cron, enabled: enabled)
end

#delete_cron_task(name) ⇒ Object

Delete a cron-task: remove both the task file and its schedule.



118
119
120
121
122
# File 'lib/clacky/server/scheduler.rb', line 118

def delete_cron_task(name)
  removed_schedule = remove_schedule(name)
  removed_task     = delete_task(name)
  removed_schedule || removed_task
end

#delete_task(task_name) ⇒ Object

Delete a task file and remove all schedules that reference it. Returns true if the task file existed and was deleted, false otherwise.



170
171
172
173
174
175
176
177
178
179
180
# File 'lib/clacky/server/scheduler.rb', line 170

def delete_task(task_name)
  path = task_file_path(task_name)
  return false unless File.exist?(path)

  File.delete(path)
  # Remove all schedules referencing this task
  load_schedules.select { |s| s["task"] == task_name }.each do |s|
    remove_schedule(s["name"])
  end
  true
end

#list_cron_tasksObject

Return a merged list of cron-tasks (task content + schedule metadata).



125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
# File 'lib/clacky/server/scheduler.rb', line 125

def list_cron_tasks
  schedule_map = load_schedules.each_with_object({}) do |s, h|
    h[s["task"]] = s if s.is_a?(Hash)
  end

  list_tasks.map do |task_name|
    content  = begin; read_task(task_name); rescue StandardError; ""; end
    schedule = schedule_map[task_name] || {}
    {
      "name"    => task_name,
      "content" => content,
      "cron"    => schedule["cron"],
      "enabled" => schedule.fetch("enabled", true),
      "scheduled" => !schedule.empty?
    }
  end
end

#list_tasksObject

List all existing task names.



160
161
162
163
164
165
166
# File 'lib/clacky/server/scheduler.rb', line 160

def list_tasks
  return [] unless Dir.exist?(TASKS_DIR)

  Dir.glob(File.join(TASKS_DIR, "*.md")).map do |path|
    File.basename(path, ".md")
  end.sort
end

#read_task(task_name) ⇒ Object

Read the prompt content of a named task.



146
147
148
149
150
151
# File 'lib/clacky/server/scheduler.rb', line 146

def read_task(task_name)
  path = task_file_path(task_name)
  raise "Task not found: #{task_name} (expected #{path})" unless File.exist?(path)

  File.read(path)
end

#remove_schedule(name) ⇒ Object

Remove a schedule entry by name.



80
81
82
83
84
85
86
# File 'lib/clacky/server/scheduler.rb', line 80

def remove_schedule(name)
  list = load_schedules
  before_count = list.size
  list.reject! { |s| s["name"] == name }
  save_schedules(list)
  list.size < before_count
end

#running?Boolean

Returns:

  • (Boolean)


54
55
56
# File 'lib/clacky/server/scheduler.rb', line 54

def running?
  @running
end

#schedulesObject

Return all schedules from the config file.



59
60
61
# File 'lib/clacky/server/scheduler.rb', line 59

def schedules
  load_schedules
end

#startObject

Start the background scheduler thread.



35
36
37
38
39
40
41
42
43
# File 'lib/clacky/server/scheduler.rb', line 35

def start
  @mutex.synchronize do
    return if @running

    @running = true
    @thread  = Thread.new { run_loop }
    @thread.name = "clacky-scheduler"
  end
end

#stopObject

Stop the background scheduler thread gracefully. NOTE: intentionally avoids Mutex here so it is safe to call from a signal trap context (Ruby disallows Mutex#synchronize inside traps).



48
49
50
51
52
# File 'lib/clacky/server/scheduler.rb', line 48

def stop
  @running = false
  @thread&.wakeup rescue nil
  @thread&.join(5)
end

#task_file_path(task_name) ⇒ Object

Return the file path for a task.



183
184
185
# File 'lib/clacky/server/scheduler.rb', line 183

def task_file_path(task_name)
  File.join(TASKS_DIR, "#{task_name}.md")
end

#update_cron_task(name, content: nil, cron: nil, enabled: nil) ⇒ Object

Update a cron-task: optionally update content and/or schedule fields.



110
111
112
113
114
115
# File 'lib/clacky/server/scheduler.rb', line 110

def update_cron_task(name, content: nil, cron: nil, enabled: nil)
  raise "Cron task not found: #{name}" unless list_tasks.include?(name)

  write_task(name, content) unless content.nil?
  update_schedule(name, cron: cron, enabled: enabled) if cron || !enabled.nil?
end

#update_schedule(name, cron: nil, enabled: nil) ⇒ Object

Update an existing schedule entry (cron and/or enabled). Returns false if the schedule does not exist.



90
91
92
93
94
95
96
97
98
99
# File 'lib/clacky/server/scheduler.rb', line 90

def update_schedule(name, cron: nil, enabled: nil)
  list = load_schedules
  entry = list.find { |s| s["name"] == name }
  return false unless entry

  entry["cron"]    = cron    unless cron.nil?
  entry["enabled"] = enabled unless enabled.nil?
  save_schedules(list)
  true
end

#write_task(task_name, content) ⇒ Object

Write the prompt content for a named task.



154
155
156
157
# File 'lib/clacky/server/scheduler.rb', line 154

def write_task(task_name, content)
  FileUtils.mkdir_p(TASKS_DIR)
  File.write(task_file_path(task_name), content)
end