lita-timing 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 0d2e91447e7269858bf093d62e62d73afc1ae09a
4
- data.tar.gz: cb8aa371af84923e55f20e10447e7fccd02ed731
3
+ metadata.gz: ebd69051f02f3a9b071894c529a1f8368b279810
4
+ data.tar.gz: e514daea0faffad2604c124e52fa7bbac9c9871c
5
5
  SHA512:
6
- metadata.gz: 330c5b008e3b22ad996837ca7dc624d93caa48fe15c9033f5f30753a036b426497812f251127fbc853d609dcbb8d2568839766ec834fd31c9e86f073456bf763
7
- data.tar.gz: 74cb5eb59284c284be1659d5b58d44f3cd811f7d4143acb03bcbd0a697441332e302905ede9e035c0a724cbd08f8c69acd0dcaf2b7a97a02807560f421d00ade
6
+ metadata.gz: 7f44239929f965ce57d97839366b7b200b1173bb88597aaa52ee5f65e0de44aa157a98745ec64bb53c163e6cdc5a1ed64c4aac6afa15627963e61d1f8bea7eb9
7
+ data.tar.gz: 5d5b54431727d312c634801198ea19709901bc596c237e86753b4327249cf06dd883c07a462d0ae6887171cfb576982ad1dfc977a3d3a8b3e14d6d23c0647683
data/README.md CHANGED
@@ -32,6 +32,40 @@ class can be used in conjunction with the built in every() helper:
32
32
  end
33
33
  end
34
34
 
35
+ ### Scheduled Timing
36
+
37
+ If you have code that should execute at a fixed time each day or week, Lita::Timing::Scheduled
38
+ can be used in conjunction with the built-in every() helper.
39
+
40
+ For daily execution:
41
+
42
+ one_minute = 60
43
+ every(one_minute) do
44
+ Lita::Timing::Scheduled.new("interval-name", redis).daily_at("11:00") do
45
+ # daily code in here
46
+ end
47
+ end
48
+
49
+ For daily execution on certain days:
50
+
51
+ one_minute = 60
52
+ every(one_minute) do
53
+ Lita::Timing::Scheduled.new("interval-name", redis).daily_at("11:00", [:monday, :tuesday]) do
54
+ # daily code in here
55
+ end
56
+ end
57
+
58
+ For weekly execution:
59
+
60
+ one_minute = 60
61
+ every(one_minute) do
62
+ Lita::Timing::Scheduled.new("interval-name", redis).weekly_at("11:00", :friday) do
63
+ # weekly code in here
64
+ end
65
+ end
66
+
67
+ All times should be specified in UTC.
68
+
35
69
  ### Sliding Windows
36
70
 
37
71
  Sometimes a handler wants to periodically execute a block of code with a start
@@ -54,3 +88,8 @@ block doesn't execute again until that window is passed.
54
88
 
55
89
  Call this as often as you like, and the block passed to advance() will
56
90
  only execute if it's been 30 minutes since the last time it executed.
91
+
92
+ ## TODO
93
+
94
+ * Add timezone support to Scheduled
95
+
@@ -0,0 +1,46 @@
1
+ require 'securerandom'
2
+
3
+ module Lita
4
+ module Timing
5
+ class Mutex
6
+
7
+ LOCK_TIMEOUT = 30 # seconds
8
+
9
+ DEL_SCRIPT = <<-EOS
10
+ if redis.call("get",KEYS[1]) == ARGV[1]
11
+ then
12
+ return redis.call("del",KEYS[1])
13
+ else
14
+ return 0
15
+ end
16
+ EOS
17
+
18
+ def initialize(name, redis)
19
+ @name, @redis = name, redis
20
+ end
21
+
22
+ def syncronise(&block)
23
+ token = SecureRandom.hex(10)
24
+ val = nil
25
+ set_with_retry(@name, token, LOCK_TIMEOUT + 1)
26
+ yield
27
+ @redis.eval(DEL_SCRIPT, keys: [@name], argv: [token])
28
+ end
29
+
30
+ private
31
+
32
+ def set_with_retry(name, token, timeout)
33
+ give_up_at = Time.now + timeout
34
+ loop do
35
+ val = @redis.set(name, token, nx: true, ex: timeout)
36
+ if val
37
+ return true
38
+ elsif Time.now > give_up_at
39
+ raise "Unable to obtain lock for #{@name} after #{LOCK_TIMEOUT + 2} seconds"
40
+ end
41
+ sleep 1
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -3,12 +3,15 @@ module Lita
3
3
  class RateLimit
4
4
  def initialize(name, redis)
5
5
  @name, @redis = name, redis
6
+ @mutex = Timing::Mutex.new("#{name}-lock", redis)
6
7
  end
7
8
 
8
9
  def once_every(seconds, &block)
9
- if last_time.nil? || last_time + seconds < Time.now
10
- yield
11
- @redis.set(@name, Time.now.to_i, ex: seconds * 2)
10
+ @mutex.syncronise do
11
+ if last_time.nil? || last_time + seconds < Time.now
12
+ yield
13
+ @redis.set(@name, Time.now.to_i, ex: seconds * 2)
14
+ end
12
15
  end
13
16
  end
14
17
 
@@ -0,0 +1,71 @@
1
+ require 'lita/timing/time_parser'
2
+ require 'lita/timing/mutex'
3
+
4
+ module Lita
5
+ module Timing
6
+ class Scheduled
7
+ ONE_WEEK_IN_SECONDS = 60 * 60 * 24 * 7
8
+
9
+ def initialize(name, redis)
10
+ @name, @redis = name, redis
11
+ @mutex = Timing::Mutex.new("#{name}-lock", redis)
12
+ end
13
+
14
+ def daily_at(time, days = nil, &block)
15
+ @mutex.syncronise do
16
+ days ||= [:monday, :tuesday, :wednesday, :thursday, :friday, :saturday, :sunday]
17
+ last_run_at = get_last_run_at
18
+ next_run = calc_next_daily_run(time, days, last_run_at)
19
+ if next_run < Time.now
20
+ yield
21
+ @redis.set(@name, Time.now.to_i, ex: ONE_WEEK_IN_SECONDS * 2)
22
+ end
23
+ end
24
+ end
25
+
26
+ def weekly_at(time, day, &block)
27
+ @mutex.syncronise do
28
+ last_run_at = get_last_run_at
29
+ next_run = calc_next_weekly_run(time, day, last_run_at)
30
+ if next_run < Time.now
31
+ yield
32
+ @redis.set(@name, Time.now.to_i, ex: ONE_WEEK_IN_SECONDS * 2)
33
+ end
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ def calc_next_daily_run(time, days, last_run_at)
40
+ hours, minutes = TimeParser.extract_hours_and_minutes(time)
41
+ wdays = TimeParser.day_strings_to_ints(days)
42
+
43
+ next_run_at = last_run_at + 1
44
+ loop do
45
+ if next_run_at.hour == hours && next_run_at.min == minutes && next_run_at.sec == 0 && wdays.include?(next_run_at.wday)
46
+ return next_run_at
47
+ end
48
+ next_run_at += 1
49
+ end
50
+ end
51
+
52
+ def calc_next_weekly_run(time, day, last_run_at)
53
+ hours, minutes = TimeParser.extract_hours_and_minutes(time)
54
+ wday = TimeParser.day_string_to_int(day)
55
+
56
+ next_run_at = last_run_at + 1
57
+ loop do
58
+ if next_run_at.hour == hours && next_run_at.min == minutes && next_run_at.sec == 0 && next_run_at.wday == wday
59
+ return next_run_at
60
+ end
61
+ next_run_at += 1
62
+ end
63
+ end
64
+
65
+ def get_last_run_at
66
+ value = @redis.get(@name)
67
+ value ? Time.at(value.to_i).utc : Time.now.utc
68
+ end
69
+ end
70
+ end
71
+ end
@@ -3,19 +3,22 @@ module Lita
3
3
  class SlidingWindow
4
4
  def initialize(name, redis)
5
5
  @name, @redis = name, redis
6
+ @mutex = Timing::Mutex.new("#{name}-lock", redis)
6
7
 
7
8
  initialise_last_time_if_not_set
8
9
  end
9
10
 
10
11
  def advance(duration_minutes: 30, buffer_minutes: 0, &block)
11
- start_time = Time.now - mins_to_seconds(duration_minutes) - mins_to_seconds(buffer_minutes)
12
- advance_to = start_time + mins_to_seconds(duration_minutes)
12
+ @mutex.syncronise do
13
+ start_time = Time.now - mins_to_seconds(duration_minutes) - mins_to_seconds(buffer_minutes)
14
+ advance_to = start_time + mins_to_seconds(duration_minutes)
13
15
 
14
- return unless start_time > last_time
16
+ return unless start_time > last_time
15
17
 
16
- yield last_time + 1, advance_to
18
+ yield last_time + 1, advance_to
17
19
 
18
- @redis.set(@name, advance_to.to_i)
20
+ @redis.set(@name, advance_to.to_i)
21
+ end
19
22
  end
20
23
 
21
24
  private
@@ -29,7 +32,9 @@ module Lita
29
32
  end
30
33
 
31
34
  def initialise_last_time_if_not_set
32
- @redis.setnx(@name, two_weeks_ago.to_i)
35
+ @mutex.syncronise do
36
+ @redis.setnx(@name, two_weeks_ago.to_i)
37
+ end
33
38
  end
34
39
 
35
40
  def two_weeks_ago
@@ -0,0 +1,36 @@
1
+ module Lita
2
+ module Timing
3
+ class TimeParser
4
+ DAYS = {
5
+ sunday: 0,
6
+ monday: 1,
7
+ tuesday: 2,
8
+ wednesday: 3,
9
+ thursday: 4,
10
+ friday: 5,
11
+ saturday: 6,
12
+ }
13
+
14
+ def self.day_string_to_int(string)
15
+ wday = DAYS[string.to_s.downcase.to_sym]
16
+ raise ArgumentError, "Expected one of: monday, tuesday, wednesday, thursday, friday, saturday or sunday" if wday.nil?
17
+ wday
18
+ end
19
+
20
+ def self.day_strings_to_ints(strings)
21
+ strings.map { |string| day_string_to_int(string) }
22
+ end
23
+
24
+ def self.extract_hours_and_minutes(string)
25
+ _, hours, minutes = *string.match(/\A(\d\d):(\d\d)\Z/)
26
+ if hours.nil? || minutes.nil?
27
+ raise ArgumentError, "time should be HH:MM"
28
+ end
29
+ if hours.to_i < 0 || hours.to_i > 23 || minutes.to_i < 0 || minutes.to_i > 59
30
+ raise ArgumentError, "time should be HH:MM"
31
+ end
32
+ return hours.to_i, minutes.to_i
33
+ end
34
+ end
35
+ end
36
+ end
data/lib/lita-timing.rb CHANGED
@@ -1,2 +1,5 @@
1
+ require 'lita/timing/mutex'
1
2
  require 'lita/timing/rate_limit'
2
3
  require 'lita/timing/sliding_window'
4
+ require 'lita/timing/time_parser'
5
+ require 'lita/timing/scheduled'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lita-timing
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - James Healy
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-06-19 00:00:00.000000000 Z
11
+ date: 2016-08-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
@@ -78,8 +78,11 @@ files:
78
78
  - MIT-LICENSE
79
79
  - README.md
80
80
  - lib/lita-timing.rb
81
+ - lib/lita/timing/mutex.rb
81
82
  - lib/lita/timing/rate_limit.rb
83
+ - lib/lita/timing/scheduled.rb
82
84
  - lib/lita/timing/sliding_window.rb
85
+ - lib/lita/timing/time_parser.rb
83
86
  homepage: http://github.com/yob/lita-timing
84
87
  licenses:
85
88
  - MIT