cocaine-framework 0.12.0.pre.rc4

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: a2b30cb410a7cdae36692be845443f20e70f6fb1
4
+ data.tar.gz: 8e2f6823087e7161aeeb049387d561eb8e1742a3
5
+ SHA512:
6
+ metadata.gz: a43592c3b4d2abf4077730fa553d8736046403f2e4ff3e78a52286cffacad5e732bec030a475db361c8c0b9f688c1630812d74b1fbf3141910307a2e2c07432e
7
+ data.tar.gz: 64fb788a2a46339f7e551e21c5e2eaaab1b582a6783ef36b3536e234de0e70fc1abf28a5a384508ddea1f253c4bdc022a4ea95e7530002ba0096974a026f1a44
@@ -0,0 +1,472 @@
1
+ require 'logger'
2
+ require 'msgpack'
3
+ require 'optparse'
4
+ require 'uri'
5
+
6
+ require 'celluloid'
7
+ require 'celluloid/io'
8
+
9
+ module Cocaine
10
+ # For dynamic method creation. [Detail].
11
+ class Meta
12
+ def metaclass
13
+ class << self
14
+ self
15
+ end
16
+ end
17
+ end
18
+
19
+ METHOD_ID = 0
20
+ TX_TREE_ID = 1
21
+ RX_TREE_ID = 2
22
+
23
+ module Default
24
+ module Locator
25
+ $host = '::'
26
+ $port = 10053
27
+ API = {
28
+ 0 => [
29
+ 'resolve',
30
+ {},
31
+ {
32
+ 0 => ['write', nil, {}],
33
+ 1 => ['error', {}, {}],
34
+ 2 => ['close', {}, {}]
35
+ }
36
+ ]
37
+ }
38
+ ERROR_CODE = 1
39
+ end
40
+ end
41
+
42
+ module RPC
43
+ HANDSHAKE, HEARTBEAT, TERMINATE, INVOKE, CHUNK, ERROR, CHOKE = (0..6).to_a
44
+
45
+ RXTREE = {
46
+ CHUNK => ['write', nil, {}],
47
+ ERROR => ['error', {}, {}],
48
+ CHOKE => ['close', {}, {}]
49
+ }
50
+ TXTREE = RXTREE
51
+ end
52
+
53
+ # [Detail]
54
+ # Base class for shared read channel state.
55
+ class Mailbox
56
+ def initialize(queue)
57
+ @queue = queue
58
+ end
59
+ end
60
+
61
+ # [API]
62
+ # Read-only part for shared reader state.
63
+ class RxMailbox < Mailbox
64
+ def recv(timeout=30.0)
65
+ @queue.receive timeout
66
+ end
67
+ end
68
+
69
+ # [Detail]
70
+ # Write-only part for shared reader state.
71
+ class TxMailbox < Mailbox
72
+ def initialize(queue, tree)
73
+ super queue
74
+
75
+ @tree = Hash.new
76
+ tree.each do |id, (method, txtree, rxtree)|
77
+ @tree[id] = txtree
78
+ end
79
+ end
80
+
81
+ def push(id, payload)
82
+ txtree = @tree[id]
83
+ if txtree && txtree.empty?
84
+ # Todo: Close.
85
+ LOG.debug "Closing RX channel #{self}"
86
+ end
87
+
88
+ @queue << [id, payload]
89
+ end
90
+ end
91
+
92
+ # [Detail]
93
+ # Shared reader state, that acts like channel. Need for channel splitting between the library and a user.
94
+ class RxChannel
95
+ attr_reader :tx, :rx
96
+
97
+ def initialize(tree)
98
+ queue = Celluloid::Mailbox.new
99
+ @tx = TxMailbox.new queue, tree
100
+ @rx = RxMailbox.new queue
101
+ end
102
+ end
103
+
104
+ # [API]
105
+ # Writer channel. Patches itself with current state, providing methods described in tx tree.
106
+ class TxChannel < Meta
107
+ def initialize(tree, session, socket)
108
+ @session = session
109
+ @socket = socket
110
+ @tree = nil
111
+ rebind tree
112
+ end
113
+
114
+ private
115
+ def push(id, *args)
116
+ LOG.debug "<- [#{@session}, #{id}, #{args}]"
117
+ @socket.write MessagePack.pack [@session, id, args]
118
+ rebind @tree[id][Cocaine::TX_TREE_ID]
119
+ end
120
+
121
+ def rebind(new)
122
+ if new.nil?
123
+ LOG.debug 'Found recursive leaf - doing nothing with tx channel'
124
+ return
125
+ end
126
+
127
+ old = @tree || Hash.new
128
+ old.each do |id, (method, txtree, rxtree)|
129
+ LOG.debug "Removed '#{method}' method for tx channel"
130
+ self.metaclass.send(:define_method, method) do |*|
131
+ raise Exception.new "Method '#{method}' is removed"
132
+ end
133
+ end
134
+
135
+ new ||= Hash.new
136
+ new.each do |id, (method, txtree, rxtree)|
137
+ LOG.debug "Defined '#{method}' method for tx channel"
138
+ self.metaclass.send(:define_method, method) do |*args|
139
+ push id, *args
140
+ end
141
+ end
142
+
143
+ @tree = new
144
+ end
145
+ end
146
+
147
+ # [Detail]
148
+ # Service actor, which can define itself via its dispatch tree.
149
+ class DefinedService < Meta
150
+ include Celluloid::IO
151
+
152
+ def initialize(name, endpoints, dispatch)
153
+ @name = name
154
+ @dispatch = dispatch
155
+
156
+ @counter = 1
157
+ @sessions = Hash.new
158
+
159
+ LOG.debug "Initializing '#{name}' service - with possible endpoints: #{endpoints}"
160
+ endpoints.each do |host, port|
161
+ LOG.debug "Trying to connect to '#{name}' at '[#{host}]:#{port}'"
162
+ begin
163
+ @endpoint = [host, port]
164
+ @socket = TCPSocket.new(host, port)
165
+ break
166
+ rescue IOError => err
167
+ LOG.warn "Failed: #{err}"
168
+ end
169
+ end
170
+
171
+ dispatch.each do |id, (method, txtree, rxtree)|
172
+ LOG.debug "Defined '#{method}' method for service #{self}"
173
+ self.metaclass.send(:define_method, method) do |*args|
174
+ LOG.debug "Invoking #{@name}.#{method}(#{args})"
175
+ return invoke(id, *args)
176
+ end
177
+ end
178
+
179
+ async.run
180
+ end
181
+
182
+ private
183
+ def run
184
+ LOG.debug "Service '#{@name}' is running"
185
+ unpacker = MessagePack::Unpacker.new
186
+ loop do
187
+ data = @socket.readpartial(4096)
188
+ unpacker.feed_each(data) do |decoded|
189
+ async.received *decoded
190
+ end
191
+ end
192
+ end
193
+
194
+ def received(session, id, payload)
195
+ LOG.debug "-> [#{session}, #{id}, #{payload}]"
196
+ tx, rx = @sessions[session]
197
+ if rx
198
+ rx.push id, payload
199
+ else
200
+ LOG.warn "Received message to closed session: [#{session}, #{id}, #{payload}]"
201
+ end
202
+ end
203
+
204
+ def invoke(id, *args)
205
+ method, txtree, rxtree = @dispatch[id]
206
+ LOG.debug "Invoking #{@name}[#{id}=#{method}] with #{args}"
207
+
208
+ txchan = TxChannel.new txtree, @counter, @socket
209
+ rxchan = RxChannel.new rxtree
210
+ @sessions[@counter] = [txchan, rxchan.tx]
211
+
212
+ LOG.debug "<- [#{@counter}, #{id}, #{args}]"
213
+ message = MessagePack.pack([@counter, id, args])
214
+ @counter += 1
215
+
216
+ @socket.write message
217
+ return txchan, rxchan.rx
218
+ end
219
+ end
220
+
221
+ # [API]
222
+ class Locator < DefinedService
223
+ def initialize(host=nil, port=nil)
224
+ super :locator, [[host || Default::Locator::host, port || Default::Locator::port]], Default::Locator::API
225
+ end
226
+ end
227
+
228
+ class ServiceError < IOError
229
+ end
230
+
231
+ # [API]
232
+ # Service class. All you need is name and (optionally) locator endpoint.
233
+ class Service < DefinedService
234
+ def initialize(name, host=nil, port=nil)
235
+ locator = Locator.new host, port
236
+ tx, rx = locator.resolve name
237
+ id, payload = rx.recv
238
+ if id == Default::Locator::ERROR_CODE
239
+ raise ServiceError.new payload
240
+ end
241
+
242
+ endpoints, version, dispatch = payload
243
+ super name, endpoints, dispatch
244
+ end
245
+ end
246
+
247
+ # [Detail]
248
+ # Special worker actor for RAII.
249
+ class WorkerActor
250
+ include Celluloid
251
+
252
+ def initialize(block)
253
+ @block = block
254
+ end
255
+
256
+ def execute(tx, rx)
257
+ @block.call tx, rx
258
+ yield
259
+ end
260
+ end
261
+
262
+ # [API]
263
+ # Worker class.
264
+ class Worker
265
+ include Celluloid
266
+ include Celluloid::IO
267
+
268
+ execute_block_on_receiver :on
269
+ finalizer :finalize
270
+
271
+ def initialize(app, uuid, endpoint)
272
+ @app = app
273
+ @uuid = uuid
274
+ @endpoint = endpoint
275
+ @actors = Hash.new
276
+ @sessions = Hash.new
277
+ end
278
+
279
+ def on(event, &block)
280
+ @actors[event.to_s] = WorkerActor.new block
281
+ end
282
+
283
+ def run
284
+ LOG.debug "Starting worker '#{@app}' with uuid '#{@uuid}' at '#{@endpoint}'"
285
+ @socket = UNIXSocket.open(@endpoint)
286
+ async.handshake
287
+ async.health
288
+ async.serve
289
+ end
290
+
291
+ private
292
+ def handshake
293
+ LOG.debug '<- Handshake'
294
+ @socket.write MessagePack::pack([1, RPC::HANDSHAKE, [@uuid]])
295
+ end
296
+
297
+ def health
298
+ heartbeat = MessagePack::pack([1, RPC::HEARTBEAT, []])
299
+ loop do
300
+ LOG.debug '<- Heartbeat'
301
+ @socket.write heartbeat
302
+ sleep 5.0
303
+ end
304
+ end
305
+
306
+ def serve
307
+ unpacker = MessagePack::Unpacker.new
308
+ loop do
309
+ data = @socket.readpartial 4096
310
+ unpacker.feed_each data do |decoded|
311
+ async.received *decoded
312
+ end
313
+ end
314
+ end
315
+
316
+ def received(session, id, payload)
317
+ LOG.debug "-> Message(#{session}, #{id}, #{payload})"
318
+ case id
319
+ when RPC::HANDSHAKE
320
+ when RPC::HEARTBEAT
321
+ when RPC::TERMINATE
322
+ terminate *payload
323
+ when RPC::INVOKE
324
+ invoke session, *payload
325
+ when RPC::CHUNK, RPC::ERROR
326
+ push session, id, *payload
327
+ when RPC::CHOKE
328
+ LOG.debug "Closing #{session} session"
329
+ @sessions.delete session
330
+ else
331
+ LOG.warn "Received unknown message: [#{session}, #{id}, #{payload}]"
332
+ end
333
+ end
334
+
335
+ def invoke(session, event)
336
+ actor = @actors[event]
337
+ txchan = TxChannel.new RPC::TXTREE, session, @socket
338
+ rxchan = RxChannel.new RPC::RXTREE
339
+
340
+ if actor
341
+ @sessions[session] = [txchan, rxchan.tx]
342
+ actor.execute txchan, rxchan.rx do
343
+ LOG.debug '<- Choke'
344
+ txchan.close
345
+ end
346
+ else
347
+ LOG.warn "Event '#{event}' is not registered"
348
+ txchan.error -1, "event '#{event}' is not registered"
349
+ end
350
+ end
351
+
352
+ def push(session, id, *payload)
353
+ tx, rx = @sessions[session]
354
+ if rx
355
+ rx.push id, *payload
356
+ else
357
+ raise Exception.new "received push event on unknown #{session} session"
358
+ end
359
+ end
360
+
361
+ def terminate(errno, reason)
362
+ LOG.warn "Terminating [#{errno}]: #{reason}"
363
+ exit errno
364
+ end
365
+
366
+ def finalize
367
+ if @socket
368
+ @socket.close
369
+ end
370
+ end
371
+ end
372
+
373
+ # [API].
374
+ class WorkerFactory
375
+ def self.create
376
+ options = {}
377
+ OptionParser.new do |opts|
378
+ opts.banner = 'Usage: <worker.rb> --app NAME --locator ADDRESS --uuid UUID --endpoint ENDPOINT'
379
+
380
+ opts.on('--app NAME', 'Worker name') do |app|
381
+ options[:app] = app
382
+ end
383
+
384
+ opts.on('--locator ADDRESS', 'Locator address') do |endpoint|
385
+ options[:locator] = endpoint
386
+ end
387
+
388
+ opts.on('--uuid UUID', 'Worker uuid') do |uuid|
389
+ options[:uuid] = uuid
390
+ end
391
+
392
+ opts.on('--endpoint ENDPOINT', 'Worker endpoint') do |endpoint|
393
+ options[:endpoint] = endpoint
394
+ end
395
+ end.parse!
396
+
397
+ Cocaine::LOG.debug "Options: #{options}"
398
+ Default::Locator::host, sep, Default::Locator::port = options[:locator].rpartition(':')
399
+ Default::Locator::port = Default::Locator::port.to_i
400
+
401
+ Cocaine::LOG.debug "Setting default Locator endpoint to: #{Default::Locator::host}:#{Default::Locator::port}"
402
+ return Worker.new(options[:app], options[:uuid], options[:endpoint])
403
+ end
404
+ end
405
+
406
+ class Rack
407
+ def self.on(event)
408
+ worker = Cocaine::WorkerFactory.create
409
+
410
+ worker.on :http do |res, req|
411
+ id, payload = req.recv
412
+ Cocaine::LOG.debug "After receive: '#{{:id => id, :payload => payload}}'"
413
+
414
+ case id
415
+ when Cocaine::RPC::CHUNK
416
+ method, url, version, headers, body = MessagePack::unpack payload
417
+ Cocaine::LOG.debug "After unpack: '#{id}, #{[method, url, version, headers, body]}'"
418
+
419
+ env = Hash[*headers.flatten]
420
+ parsed_url = URI.parse("http://#{env['Host']}#{url}")
421
+ default_host = parsed_url.hostname || 'localhost'
422
+ default_port = parsed_url.port || '80'
423
+
424
+ # noinspection RubyStringKeysInHashInspection
425
+ env.update(
426
+ {
427
+ 'GATEWAY_INTERFACE' => 'CGI/1.1',
428
+ 'PATH_INFO' => parsed_url.path || '',
429
+ 'QUERY_STRING' => parsed_url.query || '',
430
+ 'REMOTE_ADDR' => '::1',
431
+ 'REMOTE_HOST' => 'localhost',
432
+ 'REQUEST_METHOD' => method,
433
+ 'REQUEST_URI' => url,
434
+ 'SCRIPT_NAME' => '',
435
+ 'SERVER_NAME' => default_host,
436
+ 'SERVER_PORT' => default_port.to_s,
437
+ 'SERVER_PROTOCOL' => "HTTP/#{version}",
438
+ 'rack.version' => [1, 5],
439
+ 'rack.input' => body,
440
+ 'rack.errors' => $stderr,
441
+ 'rack.multithread' => true,
442
+ 'rack.multiprocess' => false,
443
+ 'rack.run_once' => false,
444
+ 'rack.url_scheme' => 'http',
445
+ 'HTTP_VERSION' => "HTTP/#{version}",
446
+ 'REQUEST_PATH' => parsed_url.path,
447
+ }
448
+ )
449
+
450
+ Cocaine::LOG.debug "ENV: #{env}"
451
+
452
+ now = Time.now
453
+ code, headers, body = yield env
454
+ headers['X-Response-Took'] = Time.now - now
455
+ res.write MessagePack.pack [code, headers.to_a]
456
+ body.each do |item|
457
+ res.write(item)
458
+ end
459
+
460
+ body.close if body.respond_to?(:close)
461
+ when Cocaine::RPC::ERROR
462
+ when Cocaine::RPC::CHOKE
463
+ else
464
+ # Type code here.
465
+ end
466
+ end
467
+
468
+ worker.run
469
+ sleep
470
+ end
471
+ end
472
+ end
@@ -0,0 +1,3 @@
1
+ module Cocaine
2
+ VERSION = '0.12.0-rc4'
3
+ end
data/lib/cocaine.rb ADDED
@@ -0,0 +1,11 @@
1
+ $:.unshift(File.dirname(__FILE__) + '/../lib')
2
+
3
+ require 'logger'
4
+
5
+ module Cocaine
6
+ LOG = Logger.new STDERR
7
+ LOG.level = ENV['WORKER_LOG_LEVEL'] || Logger::DEBUG
8
+ end
9
+
10
+ require 'cocaine/version'
11
+ require 'cocaine/cocaine'
metadata ADDED
@@ -0,0 +1,106 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cocaine-framework
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.12.0.pre.rc4
5
+ platform: ruby
6
+ authors:
7
+ - Evgeny Safronov
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-10-15 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rspec
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '3.1'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '3.1'
27
+ - !ruby/object:Gem::Dependency
28
+ name: msgpack
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '0.5'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '0.5'
41
+ - !ruby/object:Gem::Dependency
42
+ name: celluloid
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '0.16'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '0.16'
55
+ - !ruby/object:Gem::Dependency
56
+ name: celluloid-io
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '0.16'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '0.16'
69
+ description: |-
70
+ Cocaine Framework is a framework for simplifying development both server-side and client-side
71
+ applications.
72
+ email:
73
+ - division494@gmail.com
74
+ executables: []
75
+ extensions: []
76
+ extra_rdoc_files: []
77
+ files:
78
+ - lib/cocaine.rb
79
+ - lib/cocaine/cocaine.rb
80
+ - lib/cocaine/version.rb
81
+ homepage: https://github.com/cocaine/cocaine-framework-ruby
82
+ licenses:
83
+ - Ruby
84
+ - LGPLv3
85
+ metadata: {}
86
+ post_install_message:
87
+ rdoc_options: []
88
+ require_paths:
89
+ - lib
90
+ required_ruby_version: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - ">="
93
+ - !ruby/object:Gem::Version
94
+ version: '0'
95
+ required_rubygems_version: !ruby/object:Gem::Requirement
96
+ requirements:
97
+ - - ">"
98
+ - !ruby/object:Gem::Version
99
+ version: 1.3.1
100
+ requirements: []
101
+ rubyforge_project:
102
+ rubygems_version: 2.2.2
103
+ signing_key:
104
+ specification_version: 4
105
+ summary: Ruby/Cocaine library
106
+ test_files: []