ruote-beanstalk 2.1.10

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.
Binary file
Binary file
data/doc/storages.png ADDED
Binary file
@@ -0,0 +1,3 @@
1
+
2
+ require 'ruote/beanstalk'
3
+
@@ -0,0 +1,8 @@
1
+
2
+ require 'ruote'
3
+ require 'ruote/beanstalk/version'
4
+ require 'ruote/beanstalk/participant'
5
+ require 'ruote/beanstalk/receiver'
6
+ require 'ruote/beanstalk/storage'
7
+ require 'ruote/beanstalk/fork'
8
+
@@ -0,0 +1,56 @@
1
+ #--
2
+ # Copyright (c) 2005-2010, John Mettraux, jmettraux@gmail.com
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ # of this software and associated documentation files (the "Software"), to deal
6
+ # in the Software without restriction, including without limitation the rights
7
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ # copies of the Software, and to permit persons to whom the Software is
9
+ # furnished to do so, subject to the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be included in
12
+ # all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20
+ # THE SOFTWARE.
21
+ #
22
+ # Made in Japan.
23
+ #++
24
+
25
+
26
+ module Ruote
27
+ module Beanstalk
28
+
29
+ OPT_KEYS = { :address => 'l', :port => 'p', :binlog => 'b', :user => 'u' }
30
+
31
+ # :address
32
+ # :port
33
+ # :binlog
34
+ # :user
35
+ #
36
+ def self.fork (opts={})
37
+
38
+ quiet = opts.delete(:quiet)
39
+ no_kill = opts.delete(:no_kill_at_exit)
40
+
41
+ opts = opts.inject([]) { |a, (k, v)| a << "-#{OPT_KEYS[k]} #{v}" }.join(' ')
42
+
43
+ cpid = Process.fork do
44
+ puts "beanstalkd #{opts}" unless quiet
45
+ exec "beanstalkd #{opts}"
46
+ end
47
+
48
+ unless no_kill
49
+ at_exit { Process.kill(9, cpid) }
50
+ end
51
+
52
+ cpid
53
+ end
54
+ end
55
+ end
56
+
@@ -0,0 +1,147 @@
1
+ #--
2
+ # Copyright (c) 2005-2010, John Mettraux, jmettraux@gmail.com
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ # of this software and associated documentation files (the "Software"), to deal
6
+ # in the Software without restriction, including without limitation the rights
7
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ # copies of the Software, and to permit persons to whom the Software is
9
+ # furnished to do so, subject to the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be included in
12
+ # all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20
+ # THE SOFTWARE.
21
+ #
22
+ # Made in Japan.
23
+ #++
24
+
25
+ require 'beanstalk-client'
26
+
27
+ #require 'ruote/part/local_participant'
28
+
29
+
30
+ module Ruote
31
+ module Beanstalk
32
+
33
+ #
34
+ # This participant emits workitems towards a beanstalk queue.
35
+ #
36
+ # engine.register_participant(
37
+ # :heavy_labour,
38
+ # :reply_by_default => true, :beanstalk => '127.0.0.1:11300')
39
+ #
40
+ #
41
+ # == workitem format
42
+ #
43
+ # Workitems are encoded in the format
44
+ #
45
+ # [ 'workitem', workitem.to_h ]
46
+ #
47
+ # and then serialized as JSON strings.
48
+ #
49
+ #
50
+ # == cancel items
51
+ #
52
+ # Like workitems, but the format is
53
+ #
54
+ # [ 'cancelitem', fei.to_h, flavour.to_s ]
55
+ #
56
+ # where fei is the FlowExpressionId of the expression getting cancelled
57
+ # (and whose workitems are to be retired) and flavour is either 'cancel' or
58
+ # 'kill'.
59
+ #
60
+ #
61
+ # == extending this participant
62
+ #
63
+ # Extend and overwrite encode_workitem and encode_cancelitem or
64
+ # simply re-open the class and change those methods.
65
+ #
66
+ #
67
+ # == :beanstalk
68
+ #
69
+ # Indicates which beanstalk to talk to
70
+ #
71
+ # engine.register_participant(
72
+ # 'alice'
73
+ # Ruote::Beanstalk::BsParticipant,
74
+ # 'beanstalk' => '127.0.0.1:11300')
75
+ #
76
+ #
77
+ # == :tube
78
+ #
79
+ # Most of the time, you want the workitems (or the cancelitems) to be
80
+ # emitted over/in a specific tube
81
+ #
82
+ # engine.register_participant(
83
+ # 'alice'
84
+ # Ruote::Beanstalk::BsParticipant,
85
+ # 'beanstalk' => '127.0.0.1:11300',
86
+ # 'tube' => 'ruote-workitems')
87
+ #
88
+ #
89
+ # == :reply_by_default
90
+ #
91
+ # If the participant is configured with 'reply_by_default' => true, the
92
+ # participant will dispatch the workitem over to Beanstalk and then
93
+ # immediately reply to its ruote engine (letting the flow resume).
94
+ #
95
+ # engine.register_participant(
96
+ # 'alice'
97
+ # Ruote::Beanstalk::BsParticipant,
98
+ # 'beanstalk' => '127.0.0.1:11300',
99
+ # 'reply_by_default' => true)
100
+ #
101
+ class BsParticipant
102
+
103
+ include Ruote::LocalParticipant
104
+
105
+ def initialize (opts)
106
+
107
+ @opts = opts
108
+ end
109
+
110
+ def consume (workitem)
111
+
112
+ connection.put(encode_workitem(workitem))
113
+
114
+ reply(workitem) if @opts['reply_by_default']
115
+ end
116
+
117
+ def cancel (fei, flavour)
118
+
119
+ connection.put(encode_cancelitem(fei, flavour))
120
+ end
121
+
122
+ def encode_workitem (workitem)
123
+
124
+ Rufus::Json.encode([ 'workitem', workitem.to_h ])
125
+ end
126
+
127
+ def encode_cancelitem (fei, flavour)
128
+
129
+ Rufus::Json.encode([ 'cancelitem', fei.to_h, flavour.to_s ])
130
+ end
131
+
132
+ protected
133
+
134
+ def connection
135
+
136
+ con = ::Beanstalk::Connection.new(@opts['beanstalk'])
137
+
138
+ if tube = @opts['tube']
139
+ con.use(tube)
140
+ end
141
+
142
+ con
143
+ end
144
+ end
145
+ end
146
+ end
147
+
@@ -0,0 +1,149 @@
1
+ #--
2
+ # Copyright (c) 2005-2010, John Mettraux, jmettraux@gmail.com
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ # of this software and associated documentation files (the "Software"), to deal
6
+ # in the Software without restriction, including without limitation the rights
7
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ # copies of the Software, and to permit persons to whom the Software is
9
+ # furnished to do so, subject to the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be included in
12
+ # all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20
+ # THE SOFTWARE.
21
+ #
22
+ # Made in Japan.
23
+ #++
24
+
25
+ require 'beanstalk-client'
26
+
27
+ #require 'ruote/receiver/base'
28
+
29
+
30
+ module Ruote
31
+ module Beanstalk
32
+
33
+ #
34
+ # An error class for error emitted by the "remote side" and received here.
35
+ #
36
+ class BsReceiveError < RuntimeError
37
+
38
+ attr_reader :fei
39
+
40
+ def initialize (fei)
41
+ @fei = fei
42
+ super("for #{Ruote::FlowExpressionId.to_storage_id(fei)}")
43
+ end
44
+ end
45
+
46
+ #
47
+ # Whereas BsParticipant emits workitems (and cancelitems) to a Beanstalk
48
+ # queue, the receiver watches a Beanstalk queue/tube.
49
+ #
50
+ # An example initialization :
51
+ #
52
+ # Ruote::Beanstalk::BsReceiver.new(
53
+ # engine, '127.0.0.1:11300', :tube => 'out')
54
+ #
55
+ #
56
+ # == workitem format
57
+ #
58
+ # BsParticipant and BsReceiver share the same format :3
59
+ #
60
+ # [ 'workitem', workitem_as_a_hash ]
61
+ # # or
62
+ # [ 'error', error_details_as_a_string ]
63
+ #
64
+ #
65
+ # == extending this receiver
66
+ #
67
+ # Feel free to extend this class and override the listen or the process
68
+ # method.
69
+ #
70
+ #
71
+ # == :tube
72
+ #
73
+ # Indicates to the receiver which beanstalk tube it should listen to.
74
+ #
75
+ # Ruote::Beanstalk::BsReceiver.new(
76
+ # engine, '127.0.0.1:11300', :tube => 'out')
77
+ #
78
+ class BsReceiver < Ruote::Receiver
79
+
80
+ # cwes = context, worker, engine or storage
81
+ #
82
+ def initialize (cwes, beanstalk, options={})
83
+
84
+ super(cwes, options)
85
+
86
+ Thread.new do
87
+ listen(beanstalk, options['tube'] || options[:tube] || 'default')
88
+ end
89
+ end
90
+
91
+ protected
92
+
93
+ def listen (beanstalk, tube)
94
+
95
+ con = ::Beanstalk::Connection.new(beanstalk)
96
+ con.watch(tube)
97
+ con.ignore('default') unless tube == 'default'
98
+
99
+ loop do
100
+
101
+ job = con.reserve
102
+ job.delete
103
+ process(job)
104
+ end
105
+
106
+ rescue EOFError => ee
107
+ # over
108
+ end
109
+
110
+ # Is meant to return a hash with a first element that is either
111
+ # 'workitem', 'error' or 'launchitem' (a type).
112
+ # The second element depends on the type.
113
+ # It's mappend on Ruote::Beanstalk::BsParticipant anyway.
114
+ #
115
+ def decode (job)
116
+
117
+ Rufus::Json.decode(job.body)
118
+ end
119
+
120
+ def process (job)
121
+
122
+ type, data = decode(job)
123
+
124
+ if type == 'workitem'
125
+
126
+ # data holds a workitem (as a Hash)
127
+
128
+ reply(data)
129
+
130
+ elsif type == 'error'
131
+
132
+ # data holds a fei (FlowExpressionId) (as a Hash)
133
+
134
+ @context.error_handler.action_handle(
135
+ 'dispatch', data, BsReceiveError.new(data))
136
+
137
+ elsif type == 'launchitem'
138
+
139
+ pdef, fields, variables = data
140
+
141
+ launch(pdef, fields, variables)
142
+
143
+ #else simply drop
144
+ end
145
+ end
146
+ end
147
+ end
148
+ end
149
+
@@ -0,0 +1,308 @@
1
+ #--
2
+ # Copyright (c) 2005-2010, John Mettraux, jmettraux@gmail.com
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ # of this software and associated documentation files (the "Software"), to deal
6
+ # in the Software without restriction, including without limitation the rights
7
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ # copies of the Software, and to permit persons to whom the Software is
9
+ # furnished to do so, subject to the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be included in
12
+ # all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20
+ # THE SOFTWARE.
21
+ #
22
+ # Made in Japan.
23
+ #++
24
+
25
+ require 'fileutils'
26
+ require 'beanstalk-client'
27
+ #require 'ruote/storage/base'
28
+
29
+
30
+ module Ruote
31
+ module Beanstalk
32
+
33
+ #
34
+ # An error class just for BsStorage.
35
+ #
36
+ class BsStorageError < RuntimeError
37
+ end
38
+
39
+ #
40
+ # This ruote storage can be used in two modes : client and server.
41
+ #
42
+ # Beanstalk is the medium.
43
+ #
44
+ # == client
45
+ #
46
+ # The storage is pointed at a beanstalk queue
47
+ #
48
+ # engine = Ruote::Engine.new(
49
+ # Ruote::Worker.new(
50
+ # Ruote::Beanstalk::BsStorage.new('127.0.0.1:11300', opts)))
51
+ #
52
+ # All the operations (put, get, get_many, ...) of the storage are done
53
+ # by a server, connected to the same beanstalk queue.
54
+ #
55
+ # == server
56
+ #
57
+ # The storage point to a beanstalk queue and receives orders from clients
58
+ # via the queue.
59
+ #
60
+ # Ruote::Beanstalk::BsStorage.new(':11300', 'ruote_work', :fork => true)
61
+ #
62
+ # Note the directory passed as a string. When in server mode, this storage
63
+ # uses an embedded Ruote::FsStorage for the actual storage.
64
+ #
65
+ # The :fork => true lets the storage start and adjacent OS process containing
66
+ # the Beanstalk server. The storage takes care of stopping the beanstalk
67
+ # server when the Ruby process exits.
68
+ #
69
+ class BsStorage
70
+
71
+ include Ruote::StorageBase
72
+
73
+ def initialize (uri, directory=nil, options=nil)
74
+
75
+ @uri, address, port = split_uri(uri)
76
+
77
+ directory, @options = if directory.nil?
78
+ [ nil, {} ]
79
+ elsif directory.is_a?(Hash)
80
+ [ nil, directory ]
81
+ else
82
+ [ directory, options || {} ]
83
+ end
84
+
85
+ @cloche = nil
86
+
87
+ if directory
88
+ #
89
+ # run embedded Ruote::FsStorage
90
+
91
+ require 'rufus/cloche'
92
+
93
+ FileUtils.mkdir_p(directory)
94
+
95
+ @cloche = Rufus::Cloche.new(
96
+ :dir => directory, :nolock => @options['cloche_nolock'])
97
+ end
98
+
99
+ if fork_opts = @options[:fork]
100
+ #
101
+ # run beanstalk in a forked process
102
+
103
+ fork_opts = fork_opts.is_a?(Hash) ? fork_opts : {}
104
+ fork_opts = { :address => address, :port => port }.merge(fork_opts)
105
+
106
+ Ruote::Beanstalk.fork(fork_opts)
107
+
108
+ sleep 0.1
109
+ end
110
+
111
+ put_configuration
112
+
113
+ serve if @cloche
114
+ end
115
+
116
+ def put (doc, opts={})
117
+
118
+ doc.merge!('put_at' => Ruote.now_to_utc_s)
119
+
120
+ return @cloche.put(doc, opts) if @cloche
121
+
122
+ r = operate('put', [ doc ])
123
+
124
+ return r unless r.nil?
125
+
126
+ doc['_rev'] = (doc['_rev'] || -1) + 1 if opts[:update_rev]
127
+
128
+ nil
129
+ end
130
+
131
+ def get (type, key)
132
+
133
+ return @cloche.get(type, key) if @cloche
134
+
135
+ operate('get', [ type, key ])
136
+ end
137
+
138
+ def delete (doc)
139
+
140
+ return @cloche.delete(doc) if @cloche
141
+
142
+ operate('delete', [ doc ])
143
+ end
144
+
145
+ def get_many (type, key=nil, opts={})
146
+
147
+ return @cloche.get_many(type, key, opts) if @cloche
148
+
149
+ operate('get_many', [ type, key, opts ])
150
+ end
151
+
152
+ def ids (type)
153
+
154
+ return @cloche.ids(type) if @cloche
155
+
156
+ operate('ids', [ type ])
157
+ end
158
+
159
+ def purge!
160
+
161
+ if @cloche
162
+ FileUtils.rm_rf(@cloche.dir)
163
+ else
164
+ operate('purge!', [])
165
+ end
166
+ end
167
+
168
+ def dump (type)
169
+
170
+ get_many(type)
171
+ end
172
+
173
+ def shutdown
174
+
175
+ Thread.list.each do |t|
176
+ t.keys.each do |k|
177
+ next unless k.match(/^BeanstalkConnection\_/)
178
+ t[k].close
179
+ t[k] = nil
180
+ end
181
+ end
182
+ end
183
+
184
+ # Mainly used by ruote's test/unit/ut_17_storage.rb
185
+ #
186
+ def add_type (type)
187
+
188
+ # nothing to do
189
+ end
190
+
191
+ # Nukes a db type and reputs it (losing all the documents that were in it).
192
+ #
193
+ def purge_type! (type)
194
+
195
+ if @cloche
196
+ @cloche.purge_type!(type)
197
+ else
198
+ operate('purge_type!', [ type ])
199
+ end
200
+ end
201
+
202
+ protected
203
+
204
+ CONN_KEY = '__ruote_beanstalk_connection'
205
+ TUBE_NAME = 'ruote-storage-commands'
206
+
207
+ def split_uri (uri)
208
+
209
+ uri = ':' if uri == ''
210
+
211
+ address, port = uri.split(':')
212
+ address = '127.0.0.1' if address.strip == ''
213
+ port = 11300 if port.strip == ''
214
+
215
+ [ "#{address}:#{port}", address, port ]
216
+ end
217
+
218
+ def connection
219
+
220
+ c = Thread.current[CONN_KEY]
221
+ return c if c
222
+
223
+ c = ::Beanstalk::Connection.new(@uri, TUBE_NAME)
224
+ c.ignore('default')
225
+
226
+ Thread.current[CONN_KEY] = c
227
+ end
228
+
229
+ # Don't put configuration if it's already in
230
+ #
231
+ # (avoid storages from trashing configuration...)
232
+ #
233
+ def put_configuration
234
+
235
+ return if get('configurations', 'engine')
236
+
237
+ put({ '_id' => 'engine', 'type' => 'configurations' }.merge(@options))
238
+ end
239
+
240
+ def operate (command, params)
241
+
242
+ client_id = "BsStorage-#{Thread.current.object_id}-#{$$}"
243
+ timestamp = Time.now.to_f.to_s
244
+
245
+ con = connection
246
+
247
+ con.put(Rufus::Json.encode([ command, params, client_id, timestamp ]))
248
+
249
+ con.watch(client_id)
250
+ con.ignore(TUBE_NAME)
251
+
252
+ result = nil
253
+
254
+ # NOTE : what about a timeout ?
255
+
256
+ loop do
257
+
258
+ job = con.reserve
259
+ job.delete
260
+
261
+ result, ts = Rufus::Json.decode(job.body)
262
+
263
+ break if ts == timestamp # hopefully
264
+ end
265
+
266
+ if result.is_a?(Array) && result.first == 'error'
267
+ raise ArgumentError.new(result.last) if result[1] == 'ArgumentError'
268
+ raise BsStorageError.new(result.last)
269
+ end
270
+
271
+ result
272
+ end
273
+
274
+ COMMANDS = %w[ put get get_many delete ids purge! purge_type! dump ]
275
+
276
+ def serve
277
+
278
+ con = connection
279
+
280
+ loop do
281
+
282
+ job = con.reserve
283
+ job.delete
284
+
285
+ command, params, client_id, timestamp = Rufus::Json.decode(job.body)
286
+
287
+ result = begin
288
+
289
+ if COMMANDS.include?(command)
290
+ send(command, *params)
291
+ else
292
+ [ 'error', 'UnknownCommand', command ]
293
+ end
294
+
295
+ rescue Exception => e
296
+ #p e
297
+ #e.backtrace.each { |l| puts l }
298
+ [ 'error', e.class.to_s, e.to_s ]
299
+ end
300
+
301
+ con.use(client_id)
302
+ con.put(Rufus::Json.encode([ result, timestamp ]))
303
+ end
304
+ end
305
+ end
306
+ end
307
+ end
308
+