hydroponic_bean 1.1.0 → 1.2.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: 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