hydroponic_bean 1.1.0 → 1.2.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: 2f4b6d8a2f04c35eb3f1633af6644cd013a72ed8
4
- data.tar.gz: 30490d78a6e123d415091462c65d8eee5f663d67
3
+ metadata.gz: 9a0b2b9b746d52cb5ed0499c0ac2f8dc45213231
4
+ data.tar.gz: 4f23cda0c022146a7c5b99cb043363cc6a59ae8d
5
5
  SHA512:
6
- metadata.gz: 374d7a1c86c90bb0c4e11300910ef30cae7ce3b9b1867ea61df180a086049b9813b1628ffe25645cf72f2ecf7fe4a695bb0d16e4fcc6c1508bd38987f7523fde
7
- data.tar.gz: 65fbc11688e4fd1770890e6542c9d9f8d7c5b52ba8488b918518750ca6355fa57be1d2e2771d340dd3dfa174651e74bdb76554839555e0893dcb946b135855c9
6
+ metadata.gz: 0e0d194be39a4af19c053f946e9eda3740e3e1dbf17791608920abbd2bf5e2eb178804436eea5ec3c7be9e2177afef5991a52e81256133b943eab42120f16aa1
7
+ data.tar.gz: de72c9b293986ced6d10fc67952e8d1b2af2ac50896bbea77079931cc8bd4f96e1e994e82842f9cf008fbe21890bc0a9b43ff2bcd1ed12dda8ec01386f240432
@@ -0,0 +1 @@
1
+ 2.3.0
@@ -1,3 +1,9 @@
1
+ # 1.2.0
2
+
3
+ - Added all the worker commands
4
+ - Added HydroponicBean::Connection.closed?
5
+ - Now works as a beanstalk backend for Backburner!
6
+
1
7
  # 1.1.0
2
8
 
3
9
  - Added support for a lot more commands, see lib/hydroponic_bean/commands/
data/Gemfile CHANGED
@@ -1,4 +1,7 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
+ gem 'simplecov'
4
+ gem 'timecop'
5
+
3
6
  # Specify your gem's dependencies in hydroponic_bean.gemspec
4
7
  gemspec
@@ -9,5 +9,17 @@ module HydroponicBean
9
9
  include HydroponicBean::Commands::Worker
10
10
  include HydroponicBean::Commands::Tube
11
11
  include HydroponicBean::Commands::Other
12
+
13
+ # Find a job by id and yield it
14
+ #
15
+ # Outputs Protocol::NOT_FOUND if the block returns false
16
+ # or the job is not found
17
+ def for_job(id)
18
+ job = HydroponicBean.find_job(id)
19
+ if !job || !yield(job)
20
+ output(Protocol::NOT_FOUND)
21
+ return false
22
+ end
23
+ end
12
24
  end
13
25
  end
@@ -2,12 +2,13 @@ module HydroponicBean
2
2
  module Commands
3
3
  module Other
4
4
  def peek(stream, id = nil)
5
- id = id.to_i
6
- job = (id == 0) ? nil : HydroponicBean.jobs[id - 1]
7
- peek_output(job)
5
+ for_job(id) do |job|
6
+ peek_output(job)
7
+ end
8
8
  end
9
9
 
10
10
  def peek_ready(stream)
11
+ HydroponicBean.update_time!
11
12
  peek_output current_tube.ready_jobs.first
12
13
  end
13
14
 
@@ -16,46 +17,23 @@ module HydroponicBean
16
17
  end
17
18
 
18
19
  def peek_delayed(stream)
20
+ HydroponicBean.update_time!
19
21
  peek_output current_tube.delayed_jobs.first
20
22
  end
21
23
 
22
24
  def stats_job(stream, id)
23
- id = id.to_i
24
- job = (id == 0) ? nil : HydroponicBean.jobs[id - 1]
25
- if !job
26
- output(Protocol::NOT_FOUND)
27
- return false
25
+ for_job(id) do |job|
26
+ stats = job.serialize_stats.to_yaml
27
+ output("OK #{stats.length}\r\n")
28
+ output("#{stats}\r\n")
28
29
  end
29
-
30
- stats = job.serialize_stats.to_yaml
31
- output("OK #{stats.length}\r\n")
32
- output("#{stats}\r\n")
33
30
  end
34
31
 
35
32
  def kick_job(stream, id)
36
- id = id.to_i
37
- job = (id == 0) ? nil : HydroponicBean.jobs[id - 1]
38
- if !job || !job.kick
39
- output(Protocol::NOT_FOUND)
40
- return false
41
- end
42
- job.kick
43
- output("KICKED\r\n")
44
- end
45
-
46
- def watch(stream, tube_name)
47
- watched_tube_names << tube_name
48
- watched_tube_names.uniq!
49
- output("WATCHING #{watched_tube_names.count}\r\n")
50
- end
51
-
52
- def ignore(stream, tube_name)
53
- watched_tube_names.delete(tube_name)
54
- if watched_tube_names.empty?
55
- watched_tube_names << tube_name
56
- output("NOT_IGNORED\r\n")
57
- else
58
- output("WATCHING #{watched_tube_names.count}\r\n")
33
+ for_job(id) do |job|
34
+ if job.kick
35
+ output("KICKED\r\n")
36
+ end
59
37
  end
60
38
  end
61
39
 
@@ -7,6 +7,9 @@ module HydroponicBean
7
7
  end
8
8
 
9
9
  def put(stream, pri, delay, ttr, bytes)
10
+ # Mark this connection as a producer
11
+ producer!
12
+
10
13
  bytes = bytes.to_i
11
14
  data = stream.read(bytes)
12
15
 
@@ -20,8 +23,8 @@ module HydroponicBean
20
23
  return false
21
24
  end
22
25
 
23
- id = create_job(pri, delay, ttr, data)
24
- output("INSERTED #{id}\r\n")
26
+ job = create_job(pri, delay, ttr, data)
27
+ output("INSERTED #{job.id}\r\n")
25
28
  end
26
29
  end
27
30
  end
@@ -1,17 +1,6 @@
1
1
  module HydroponicBean
2
2
  module Commands
3
3
  module Tube
4
- def delete(stream, id)
5
- job = HydroponicBean.jobs[id.to_i - 1]
6
- if job
7
- job.delete
8
- output("DELETED\r\n")
9
- else
10
- output(Protocol::NOT_FOUND)
11
- return false
12
- end
13
- end
14
-
15
4
  def list_tubes(stream)
16
5
  tubes = HydroponicBean.tubes.keys.to_yaml
17
6
  output("OK #{tubes.length}\r\n")
@@ -48,6 +37,7 @@ module HydroponicBean
48
37
  end
49
38
 
50
39
  def kick(stream, bound)
40
+ HydroponicBean.update_time!
51
41
  bound = bound.to_i
52
42
  tube = current_tube
53
43
  output("KICKED #{tube.kick(bound)}\r\n")
@@ -1,6 +1,82 @@
1
1
  module HydroponicBean
2
2
  module Commands
3
3
  module Worker
4
+ def reserve(stream)
5
+ reserve_with_timeout(stream, -1)
6
+ end
7
+
8
+ def reserve_with_timeout(stream, seconds)
9
+ # Mark this connection as a worker
10
+ worker!
11
+
12
+ if deadline_soon?
13
+ output("DEADLINE_SOON\r\n")
14
+ return true
15
+ end
16
+
17
+ seconds = seconds.to_i
18
+
19
+ if job = wait_for_job(seconds)
20
+ output("RESERVED #{job.id} #{job.data.length}\r\n")
21
+ output("#{job.data}\r\n")
22
+ else
23
+ output("TIMED_OUT\r\n")
24
+ end
25
+ end
26
+
27
+ def release(stream, id, pri, delay)
28
+ # We don't have a BURIED response here
29
+ for_job(id) do |job|
30
+ if job.release(self, pri, delay)
31
+ output("RELEASED\r\n")
32
+ end
33
+ end
34
+ end
35
+
36
+ def bury(stream, id, pri)
37
+ for_job(id) do |job|
38
+ if job.bury(self, pri)
39
+ output("BURIED\r\n")
40
+ end
41
+ end
42
+ end
43
+
44
+ def delete(stream, id)
45
+ for_job(id) do |job|
46
+ if job.delete(self)
47
+ output("DELETED\r\n")
48
+ end
49
+ end
50
+ end
51
+
52
+ def touch(stream, id)
53
+ for_job(id) do |job|
54
+ if job.touch(self)
55
+ output("TOUCHED\r\n")
56
+ end
57
+ end
58
+ end
59
+
60
+ def watch(stream, tube_name)
61
+ watched_tube_names << tube_name
62
+ watched_tube_names.uniq!
63
+ output_watching
64
+ end
65
+
66
+ def ignore(stream, tube_name)
67
+ watched_tube_names.delete(tube_name)
68
+ if watched_tube_names.empty?
69
+ watched_tube_names << tube_name
70
+ output("NOT_IGNORED\r\n")
71
+ else
72
+ output_watching
73
+ end
74
+ end
75
+
76
+ protected
77
+ def output_watching
78
+ output("WATCHING #{watched_tube_names.count}\r\n")
79
+ end
4
80
  end
5
81
  end
6
82
  end
@@ -4,8 +4,23 @@ module HydroponicBean
4
4
  class Connection
5
5
  include HydroponicBean::Protocol
6
6
 
7
+ attr_accessor :waiting
8
+ alias_method :waiting?, :waiting
9
+
7
10
  def initialize
8
11
  @_read, @_write = IO.pipe
12
+ @worker, @producer = false
13
+ @waiting = false
14
+ HydroponicBean.add_connection(self)
15
+ end
16
+
17
+ def worker?; @worker; end
18
+ def worker!; @worker = true; end
19
+ def producer?; @producer; end
20
+ def producer!; @producer = true; end
21
+
22
+ def closed?
23
+ @_write.closed?
9
24
  end
10
25
 
11
26
  # Necessary interface used by beaneater
@@ -24,6 +39,7 @@ module HydroponicBean
24
39
  def close
25
40
  @_read.close
26
41
  @_write.close
42
+ HydroponicBean.remove_connection(self)
27
43
  end
28
44
 
29
45
  protected
@@ -1,15 +1,56 @@
1
+ require 'timeout'
2
+
1
3
  require 'hydroponic_bean/tube'
2
4
  require 'hydroponic_bean/job'
3
5
 
4
6
  module HydroponicBean
7
+ def self.clear
8
+ tubes.clear
9
+ jobs.clear
10
+ connections.clear
11
+ commands.clear
12
+ end
13
+
5
14
  def self.tubes
6
15
  @tubes ||= Hash.new{|h, k| h[k] = Tube.new(k)}
7
16
  end
8
17
 
18
+ def self.find_job(id)
19
+ id = id.to_i
20
+ if id == 0
21
+ return nil
22
+ else
23
+ job = jobs[id - 1]
24
+ job&.update_time!
25
+ return job
26
+ end
27
+ end
28
+
9
29
  def self.jobs
10
30
  @jobs ||= []
11
31
  end
12
32
 
33
+ def self.update_time!
34
+ jobs.each(&:update_time!)
35
+ end
36
+
37
+ def self.connections
38
+ @connections ||= []
39
+ end
40
+
41
+ def self.add_connection(connection)
42
+ connections.push(connection)
43
+ end
44
+
45
+ def self.remove_connection(connection)
46
+ connections.delete(connection)
47
+ end
48
+
49
+ # Keep track of commands for stats
50
+ def self.commands
51
+ @commands ||= Hash.new{|h, k| h[k] = 0}
52
+ end
53
+
13
54
  module Data
14
55
  def current_tube_name
15
56
  @current_tube_name ||= 'default'
@@ -23,12 +64,69 @@ module HydroponicBean
23
64
  @watched_tube_names ||= ['default']
24
65
  end
25
66
 
67
+ def watched_tubes
68
+ watched_tube_names.map do |name|
69
+ HydroponicBean.tubes[name]
70
+ end
71
+ end
72
+
26
73
  def create_job(pri, delay, ttr, data)
27
74
  job = Job.new(current_tube, pri, delay, ttr, data)
28
75
 
29
76
  HydroponicBean.jobs.push(job)
30
77
 
31
- return job.id
78
+ return job
79
+ end
80
+
81
+ def deadline_soon?
82
+ HydroponicBean.update_time!
83
+ watched_tubes.map(&:reserved_jobs).flatten.select do |job|
84
+ job.reserved_by == self
85
+ end.sort_by(&:ttr_left).first&.deadline_soon?
86
+ end
87
+
88
+ def reserve_job
89
+ HydroponicBean.update_time!
90
+ reservable_jobs.first&.reserve(self)
91
+ end
92
+
93
+ def reservable_jobs
94
+ watched_tubes.reject(&:paused?).map(&:ready_jobs).flatten.sort_by(&:created_at).sort_by(&:pri)
95
+ end
96
+
97
+ def wait_for_job(timeout)
98
+ self.waiting = true
99
+ if timeout == 0
100
+ return reserve_job
101
+ else
102
+ timeout = [0, timeout].max
103
+ Timeout.timeout(timeout) do
104
+ while !(job = reserve_job)
105
+ sleep 0.49
106
+ end
107
+ return job
108
+ end
109
+ end
110
+ rescue Timeout::Error
111
+ return nil
112
+ ensure
113
+ self.waiting = false
114
+ end
115
+
116
+ def stats
117
+ {
118
+ 'current-jobs-urgent' => HydroponicBean.jobs.select(&:urgent?).count,
119
+ 'current-jobs-ready' => HydroponicBean.jobs.select(&:ready?).count,
120
+ 'current-jobs-reserved' => HydroponicBean.jobs.select(&:reserved?).count,
121
+ 'current-jobs-delayed' => HydroponicBean.jobs.select(&:delayed?).count,
122
+ 'current-jobs-buried' => HydroponicBean.jobs.select(&:buried?).count,
123
+ 'total-jobs' => HydroponicBean.jobs.count,
124
+ 'current-tubes' => HydroponicBean.tubes.count,
125
+ 'current-connections' => HydroponicBean.connections.count,
126
+ 'current-producers' => HydroponicBean.connections.select(&:produced?).count,
127
+ 'current-workers' => HydroponicBean.connections.select(&:workers?).count,
128
+ 'current-waiting' => HydroponicBean.connections.select(&:waiting?).count,
129
+ }.merge(Hash[HydroponicBean.commands.map{|k, v| ["cmd-#{k}", v]}])
32
130
  end
33
131
  end
34
132
  end
@@ -5,9 +5,7 @@ module HydroponicBean
5
5
  end
6
6
 
7
7
  attr_accessor :id, :pri, :delay, :ttr, :data, :created_at,
8
- :state, :tube, :stats
9
-
10
- attr_reader :deleted
8
+ :reserved_at, :reserved_by, :state, :tube, :stats
11
9
 
12
10
  def initialize(tube, pri, delay, ttr, data)
13
11
  @id = self.class.next_id
@@ -15,6 +13,7 @@ module HydroponicBean
15
13
  @tube = tube
16
14
  @pri = pri.to_i
17
15
  @delay = delay.to_i
16
+ @reserved_at = nil
18
17
  @ttr = ttr.to_i
19
18
  @state = @delay > 0 ? State.delayed : State.ready
20
19
  @data = data
@@ -30,6 +29,15 @@ module HydroponicBean
30
29
  @tube.push(self)
31
30
  end
32
31
 
32
+ def update_time!
33
+ if (delayed? || reserved?) && time_left == 0
34
+ if reserved?
35
+ stats['timeouts'] += 1
36
+ end
37
+ @state = State.ready
38
+ end
39
+ end
40
+
33
41
  def age
34
42
  (Time.now.utc - created_at).to_i
35
43
  end
@@ -44,13 +52,54 @@ module HydroponicBean
44
52
  def delayed?; exists? && state == State.delayed; end
45
53
  def buried?; exists? && state == State.buried; end
46
54
 
47
- def delete
48
- if @deleted == false
49
- @tube.job_deleted
55
+ def reserved_by?(connection)
56
+ reserved? && reserved_by == connection
57
+ end
58
+
59
+ def reserve(connection)
60
+ if ready?
61
+ stats['reserves'] += 1
62
+ @state = State.reserved
63
+ @reserved_by = connection
64
+ @reserved_at = Time.now.utc
65
+ # For convenience and one-liners
66
+ return self
67
+ end
68
+ end
69
+
70
+ def release(connection, pri, delay)
71
+ if reserved_by?(connection)
72
+ stats['releases'] += 1
73
+ @pri = pri.to_i
74
+ @delay = delay.to_i
75
+ @reserved_at = nil
76
+ @reserved_by = nil
77
+ @state = @delay > 0 ? State.delayed : State.ready
78
+ end
79
+ end
80
+
81
+ def bury(connection, pri)
82
+ if reserved_by?(connection)
83
+ stats['buries'] += 1
84
+ @pri = pri.to_i
85
+ @reserved_at = nil
86
+ @reserved_by = nil
87
+ @state = State.buried
88
+ end
89
+ end
90
+
91
+ def delete(connection)
92
+ if exists? && (!reserved? || reserved_by?(connection))
93
+ @tube.job_deleted!
50
94
  @deleted = true
51
95
  end
52
96
  end
53
- def exists?; !deleted; end
97
+
98
+ def touch(connection)
99
+ if reserved_by?(connection)
100
+ @reserved_at = Time.now.utc
101
+ end
102
+ end
54
103
 
55
104
  def kick
56
105
  if buried? || delayed?
@@ -59,8 +108,16 @@ module HydroponicBean
59
108
  end
60
109
  end
61
110
 
111
+ def ttr_left
112
+ [ttr - (Time.now.utc - reserved_at).to_i, 0].max
113
+ end
114
+
62
115
  def time_left
63
- reserved? ? ttr : [delay - age, 0].max
116
+ reserved? ? ttr_left : [delay - age, 0].max
117
+ end
118
+
119
+ def deadline_soon?
120
+ ttr_left <= 1
64
121
  end
65
122
 
66
123
  def serialize_stats
@@ -77,6 +134,9 @@ module HydroponicBean
77
134
  }.merge(stats)
78
135
  end
79
136
 
137
+ def deleted?; @deleted; end
138
+ def exists?; !@deleted; end
139
+
80
140
  module State
81
141
  def self.ready; :ready; end
82
142
  def self.reserved; :reserved; end