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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: c1c534abd255cca4223c3d12ac9fed05cbb065a2
4
- data.tar.gz: 4bc8e533d727d056ee1430a2c1354cfa9aeec931
3
+ metadata.gz: 23f66b45e3100b728a6a53a23fa9f166552d7989
4
+ data.tar.gz: 4b23ad6c9ed0638905152bb6ac960fa98978a47d
5
5
  SHA512:
6
- metadata.gz: 7701aaef31efb81503dc8eb0f6e4505d8429335757e0b3cbd118747bf6ac8d48aed92a195731b621cdfcc797f7f185b5b68c8741c9a836dffbd4e05d1dbc00db
7
- data.tar.gz: a865d1c5ddc0cab514cdfbc2b219e43a3979619ab88b741736627c0d1265c796ce62bdfbd51ae088ed2ec9bff28c2485ff8ab9d5a2f54c05b47f61b823a582a2
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 /^schedule\s+"(.+)"\s+in\s+(.+)$/i, :schedule
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
- def schedule(payload)
8
- payload.matches.each do |task, timing|
9
- serialized = serialize_message(payload.message, new_body: task)
10
- resend(serialized)
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..10000)}"
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(:user_name))
26
- room = Lita::Room.new(serialized.fetch(:room_name))
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(:body)}"
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
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |spec|
2
2
  spec.name = "lita-task-scheduler"
3
- spec.version = "0.1.1"
3
+ spec.version = "1.0.0"
4
4
  spec.authors = ["Daniel J. Pritchett"]
5
5
  spec.email = ["dpritchett@gmail.com"]
6
6
  spec.description = "Schedule jobs in Lita"
@@ -1,4 +1,115 @@
1
- require "spec_helper"
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.1.1
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-03-14 00:00:00.000000000 Z
11
+ date: 2018-04-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: lita