lita-task-scheduler 0.1.1 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/lita/handlers/task_scheduler.rb +128 -9
- data/lita-task-scheduler.gemspec +1 -1
- data/spec/lita/handlers/task_scheduler_spec.rb +112 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 23f66b45e3100b728a6a53a23fa9f166552d7989
|
4
|
+
data.tar.gz: 4b23ad6c9ed0638905152bb6ac960fa98978a47d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 468d8b5438c5d815e570d72d14c727b093f7d584104017c459b1c0a72d2ec6097fba77df538f2e02acfccb7e1615c3b7ab130419845363dbd23ffcc12aff929d
|
7
|
+
data.tar.gz: 969dc2317064912351a8bdf4af531fd0e1095186d4100298587b298c0cf5c027339672e2311ad08481dad6757b178ec8f4df5d897e5dbd54ee54739f4acb6c6e
|
@@ -1,20 +1,135 @@
|
|
1
|
+
require 'pry'
|
2
|
+
require 'json'
|
3
|
+
|
1
4
|
module Lita
|
2
5
|
module Handlers
|
3
6
|
class TaskScheduler < Handler
|
4
7
|
|
5
|
-
route
|
8
|
+
route(/^schedule\s+"(.+)"\s+in\s+(.+)$/i, :schedule_command, command: true)
|
9
|
+
route(/^show schedule$/i, :show_schedule, command: true)
|
10
|
+
route(/^empty schedule$/i, :empty_schedule, command: true)
|
11
|
+
|
12
|
+
REDIS_TASKS_KEY = name.to_s
|
13
|
+
|
14
|
+
def show_schedule(payload)
|
15
|
+
schedule = redis.hgetall(REDIS_TASKS_KEY)
|
16
|
+
|
17
|
+
payload.reply schedule_report(schedule)
|
18
|
+
end
|
19
|
+
|
20
|
+
def empty_schedule(payload)
|
21
|
+
redis.del(REDIS_TASKS_KEY)
|
22
|
+
show_schedule payload
|
23
|
+
end
|
24
|
+
|
25
|
+
def schedule_report(schedule)
|
26
|
+
reply = 'Scheduled tasks: '
|
27
|
+
descriptions = []
|
28
|
+
|
29
|
+
schedule.keys.each do |timestamp|
|
30
|
+
play_time = Time.at(timestamp.to_i)
|
31
|
+
tasks_json = schedule[timestamp]
|
32
|
+
tasks = JSON.parse(tasks_json)
|
33
|
+
|
34
|
+
tasks.each do |task|
|
35
|
+
descriptions << "\n - \"#{task.fetch('body')}\" at #{play_time}"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
reply + (descriptions.empty? ? 'None.' : descriptions.join)
|
40
|
+
end
|
41
|
+
|
42
|
+
def schedule_command(payload)
|
43
|
+
task, timing = payload.matches.last
|
44
|
+
run_at = parse_timing(timing)
|
45
|
+
serialized = serialize_message(payload.message, new_body: task)
|
46
|
+
|
47
|
+
defer_task(serialized, run_at)
|
48
|
+
show_schedule(payload)
|
49
|
+
end
|
50
|
+
|
51
|
+
def defer_task(serialized_task, run_at)
|
52
|
+
key_time = run_at.to_i.to_s
|
53
|
+
|
54
|
+
redis.watch(REDIS_TASKS_KEY)
|
55
|
+
|
56
|
+
tasks = redis.hget(REDIS_TASKS_KEY, key_time) || []
|
57
|
+
|
58
|
+
tasks = JSON.parse(tasks) unless tasks.empty?
|
59
|
+
tasks << serialized_task
|
60
|
+
|
61
|
+
redis.hset(REDIS_TASKS_KEY, key_time, tasks.to_json)
|
6
62
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
63
|
+
redis.unwatch
|
64
|
+
|
65
|
+
tasks
|
66
|
+
end
|
67
|
+
|
68
|
+
def execute_tasks(serialized_tasks)
|
69
|
+
serialized_tasks.each do |serialized_task|
|
70
|
+
Lita.logger.debug "Resending task #{serialized_task}"
|
71
|
+
resend serialized_task
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def run_loop
|
76
|
+
Thread.new do
|
77
|
+
loop do
|
78
|
+
tick
|
79
|
+
sleep 1
|
80
|
+
end
|
11
81
|
end
|
12
82
|
end
|
13
83
|
|
84
|
+
def tick
|
85
|
+
tasks = find_tasks_due
|
86
|
+
tasks.each { |t| resend t }
|
87
|
+
Lita.logger.debug "Task loop done for #{Time.now}"
|
88
|
+
end
|
89
|
+
|
90
|
+
def find_tasks_due
|
91
|
+
results = []
|
92
|
+
timestamps = redis.hkeys(REDIS_TASKS_KEY)
|
93
|
+
|
94
|
+
timestamps.each do |t|
|
95
|
+
key_time = Time.at(t.to_i)
|
96
|
+
next unless key_time <= Time.now
|
97
|
+
|
98
|
+
tasks_raw = redis.hget(REDIS_TASKS_KEY, t)
|
99
|
+
tasks = JSON.parse(tasks_raw)
|
100
|
+
|
101
|
+
results += tasks
|
102
|
+
redis.hdel(REDIS_TASKS_KEY, t)
|
103
|
+
end
|
104
|
+
|
105
|
+
results
|
106
|
+
end
|
107
|
+
|
108
|
+
def parse_timing(timing)
|
109
|
+
count, unit = timing.split
|
110
|
+
count = count.to_i
|
111
|
+
unit = unit.downcase.strip.gsub(/s$/, '')
|
112
|
+
|
113
|
+
seconds = case unit
|
114
|
+
when 'second'
|
115
|
+
count
|
116
|
+
when 'minute'
|
117
|
+
count * 60
|
118
|
+
when 'hour'
|
119
|
+
count * 60 * 60
|
120
|
+
when 'day'
|
121
|
+
count * 60 * 60 * 24
|
122
|
+
else
|
123
|
+
raise ArgumentError, "I don't recognize #{unit}"
|
124
|
+
end
|
125
|
+
|
126
|
+
Time.now.utc + seconds
|
127
|
+
end
|
128
|
+
|
14
129
|
def rebroadcast(payload)
|
15
130
|
serialized = serialize_message(payload.message)
|
16
131
|
|
17
|
-
key = "delay_#{rand(100..
|
132
|
+
key = "delay_#{rand(100..10_000)}"
|
18
133
|
redis.set(key, serialized.to_json)
|
19
134
|
reloaded = JSON.parse redis.get(key), symbolize_names: true
|
20
135
|
|
@@ -22,10 +137,10 @@ module Lita
|
|
22
137
|
end
|
23
138
|
|
24
139
|
def resend(serialized)
|
25
|
-
user = Lita::User.new(serialized.fetch(
|
26
|
-
room = Lita::Room.new(serialized.fetch(
|
140
|
+
user = Lita::User.new(serialized.fetch('user_name'))
|
141
|
+
room = Lita::Room.new(serialized.fetch('room_name'))
|
27
142
|
source = Lita::Source.new(user: user, room: room)
|
28
|
-
body = "#{robot.name} #{serialized.fetch(
|
143
|
+
body = "#{robot.name} #{serialized.fetch('body')}"
|
29
144
|
|
30
145
|
newmsg = Lita::Message.new(
|
31
146
|
robot,
|
@@ -45,6 +160,10 @@ module Lita
|
|
45
160
|
end
|
46
161
|
|
47
162
|
Lita.register_handler(self)
|
163
|
+
|
164
|
+
on :loaded do
|
165
|
+
run_loop
|
166
|
+
end
|
48
167
|
end
|
49
168
|
end
|
50
169
|
end
|
data/lita-task-scheduler.gemspec
CHANGED
@@ -1,4 +1,115 @@
|
|
1
|
-
require
|
1
|
+
require 'spec_helper'
|
2
2
|
|
3
3
|
describe Lita::Handlers::TaskScheduler, lita_handler: true do
|
4
|
+
let(:robot) { Lita::Robot.new(registry) }
|
5
|
+
|
6
|
+
subject { described_class.new(robot) }
|
7
|
+
|
8
|
+
describe 'routing' do
|
9
|
+
it { is_expected.to route('Lita schedule "show schedule" in 2 hours') }
|
10
|
+
it { is_expected.to route('Lita show schedule') }
|
11
|
+
end
|
12
|
+
|
13
|
+
describe 'functionality' do
|
14
|
+
|
15
|
+
describe ':schedule_report' do
|
16
|
+
context 'tasks are scheduled'
|
17
|
+
before do
|
18
|
+
send_message 'Lita schedule "show schedule" in 1 seconds'
|
19
|
+
send_message 'Lita schedule "show schedule" in 6 seconds'
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'should list scheduled tasks on demand' do
|
23
|
+
message = double 'message'
|
24
|
+
expect(message).to receive(:reply)
|
25
|
+
subject.show_schedule(message)
|
26
|
+
send_message 'Lita show schedule'
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
describe ':defer_task' do
|
31
|
+
it 'defers any single task' do
|
32
|
+
message = { canary_message: Time.now }
|
33
|
+
run_at = Time.now + 5
|
34
|
+
result = subject.defer_task(message, run_at)
|
35
|
+
expect(result).to include(message)
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'stores multiple same-second tasks in an array' do
|
39
|
+
message = { 'canary_message' => Time.now.to_i }
|
40
|
+
run_at = Time.now + 5
|
41
|
+
|
42
|
+
5.times do
|
43
|
+
subject.defer_task(message, run_at)
|
44
|
+
end
|
45
|
+
|
46
|
+
result = subject.defer_task(message, run_at)
|
47
|
+
|
48
|
+
expect(result).to eq([message] * 6)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
describe ':find_tasks_due' do
|
53
|
+
context 'two tasks are scheduled for five seconds ago' do
|
54
|
+
before { 2.times { subject.defer_task('past_task', Time.now - 5) } }
|
55
|
+
|
56
|
+
it 'returns all past due tasks' do
|
57
|
+
result = subject.find_tasks_due
|
58
|
+
expected = %w[past_task past_task]
|
59
|
+
expect(result).to eq(expected)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
context 'one task scheduled in the future' do
|
64
|
+
before { subject.defer_task('future_task', Time.now + 100) }
|
65
|
+
|
66
|
+
it 'does not return that new task' do
|
67
|
+
result = subject.find_tasks_due
|
68
|
+
expect(result).to_not include('future_task')
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
describe 'execute_tasks' do
|
75
|
+
it 'resends each task' do
|
76
|
+
tasks = [{}, {}]
|
77
|
+
|
78
|
+
expect(subject).to receive(:resend).exactly(2).times
|
79
|
+
subject.execute_tasks(tasks)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
describe 'tick' do
|
84
|
+
before { subject.stub(:find_tasks_due).and_return ['a_task'] }
|
85
|
+
|
86
|
+
it 'should find tasks due and resend them' do
|
87
|
+
expect(subject).to receive(:find_tasks_due)
|
88
|
+
expect(subject).to receive(:resend).with('a_task')
|
89
|
+
|
90
|
+
subject.tick
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
describe ':parse_timing' do
|
95
|
+
def time_drift(time, expected_seconds:)
|
96
|
+
delta = (Time.now.utc - time).abs
|
97
|
+
(expected_seconds - delta).abs
|
98
|
+
end
|
99
|
+
|
100
|
+
it 'parses seconds, minutes, hours, and days' do
|
101
|
+
test_table = [
|
102
|
+
['3 seconds', 3],
|
103
|
+
['4 minutes', 4 * 60],
|
104
|
+
['2 hours', 2 * 60 * 60],
|
105
|
+
['5 days', 5 * 60 * 60 * 24]
|
106
|
+
]
|
107
|
+
|
108
|
+
test_table.each do |input, expected|
|
109
|
+
response = subject.parse_timing(input)
|
110
|
+
drift = time_drift(response, expected_seconds: expected)
|
111
|
+
expect(drift < 0.1).to be_truthy, "#{input}:\t #{expected}"
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
4
115
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: lita-task-scheduler
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Daniel J. Pritchett
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-
|
11
|
+
date: 2018-04-07 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: lita
|