cocaine-framework 0.12.0.pre.rc4

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 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: []