ruote-beanstalk 2.1.10

Sign up to get free protection for your applications and to get access to all the features.
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
+