cramp 0.12 → 0.13

Sign up to get free protection for your applications and to get access to all the features.
data/lib/cramp.rb CHANGED
@@ -2,6 +2,7 @@ require 'eventmachine'
2
2
  EM.epoll
3
3
 
4
4
  require 'active_support'
5
+ require 'active_support/core_ext/class/attribute'
5
6
  require 'active_support/core_ext/class/inheritable_attributes'
6
7
  require 'active_support/core_ext/class/attribute_accessors'
7
8
  require 'active_support/core_ext/module/aliasing'
@@ -13,17 +14,25 @@ require 'active_support/buffered_logger'
13
14
 
14
15
  require 'rack'
15
16
 
17
+ if RUBY_VERSION >= '1.9.1'
18
+ require File.join(File.dirname(__FILE__), 'vendor/fiber_pool')
19
+ end
20
+
16
21
  module Cramp
17
- VERSION = '0.12'
22
+ VERSION = '0.13'
18
23
 
19
24
  mattr_accessor :logger
20
25
 
21
26
  autoload :Action, "cramp/action"
22
27
  autoload :Websocket, "cramp/websocket"
28
+ autoload :WebsocketExtension, "cramp/websocket/extension"
29
+ autoload :SSE, "cramp/sse"
30
+ autoload :LongPolling, "cramp/long_polling"
23
31
  autoload :Body, "cramp/body"
24
32
  autoload :PeriodicTimer, "cramp/periodic_timer"
25
33
  autoload :KeepConnectionAlive, "cramp/keep_connection_alive"
26
34
  autoload :Abstract, "cramp/abstract"
27
35
  autoload :Callbacks, "cramp/callbacks"
36
+ autoload :FiberPool, "cramp/fiber_pool"
28
37
  autoload :TestCase, "cramp/test_case"
29
38
  end
@@ -2,10 +2,11 @@ require 'active_support/core_ext/hash/keys'
2
2
 
3
3
  module Cramp
4
4
  class Abstract
5
-
6
5
  include Callbacks
6
+ include FiberPool
7
7
 
8
- ASYNC_RESPONSE = [-1, {}, []].freeze
8
+ class_attribute :transport
9
+ self.transport = :regular
9
10
 
10
11
  class << self
11
12
  def call(env)
@@ -15,20 +16,22 @@ module Cramp
15
16
 
16
17
  def initialize(env)
17
18
  @env = env
19
+ @finished = false
18
20
  end
19
21
 
20
22
  def process
21
23
  EM.next_tick { before_start }
22
- ASYNC_RESPONSE
24
+ throw :async
23
25
  end
24
26
 
27
+ protected
28
+
25
29
  def continue
26
30
  init_async_body
27
31
 
28
32
  status, headers = respond_with
29
33
  send_initial_response(status, headers, @body)
30
34
 
31
- EM.next_tick { start } if respond_to?(:start)
32
35
  EM.next_tick { on_start }
33
36
  end
34
37
 
@@ -45,16 +48,25 @@ module Cramp
45
48
  end
46
49
  end
47
50
 
51
+ def finished?
52
+ !!@finished
53
+ end
54
+
48
55
  def finish
56
+ @finished = true
49
57
  @body.succeed
50
58
  end
51
59
 
52
60
  def send_initial_response(response_status, response_headers, response_body)
53
- EM.next_tick { @env['async.callback'].call [response_status, response_headers, response_body] }
61
+ send_response(response_status, response_headers, response_body)
54
62
  end
55
63
 
56
64
  def halt(status, headers = {}, halt_body = '')
57
- send_initial_response(status, headers, halt_body)
65
+ send_response(status, headers, halt_body)
66
+ end
67
+
68
+ def send_response(response_status, response_headers, response_body)
69
+ @env['async.callback'].call [response_status, response_headers, response_body]
58
70
  end
59
71
 
60
72
  def request
@@ -66,7 +78,7 @@ module Cramp
66
78
  end
67
79
 
68
80
  def route_params
69
- @env['router.params']||@env['usher.params']
81
+ @env['router.params'] || @env['usher.params']
70
82
  end
71
83
  end
72
84
  end
data/lib/cramp/action.rb CHANGED
@@ -1,12 +1,60 @@
1
1
  module Cramp
2
2
  class Action < Abstract
3
-
4
3
  include PeriodicTimer
5
4
  include KeepConnectionAlive
6
5
 
7
- def render(body)
6
+ protected
7
+
8
+ def render(body, *args)
9
+ send(:"render_#{transport}", body, *args)
10
+ end
11
+
12
+ def send_initial_response(*)
13
+ case transport
14
+ when :long_polling
15
+ # Dont send no initial response
16
+ else
17
+ super
18
+ end
19
+ end
20
+
21
+ def respond_with
22
+ case transport
23
+ when :sse
24
+ [200, {'Content-Type' => 'text/event-stream', 'Cache-Control' => 'no-cache', 'Connection' => 'keep-alive'}]
25
+ else
26
+ super
27
+ end
28
+ end
29
+
30
+ def render_regular(body, *)
8
31
  @body.call(body)
9
32
  end
10
33
 
34
+ def render_long_polling(data, *)
35
+ status, headers = respond_with
36
+ headers['Content-Length'] = data.size.to_s
37
+
38
+ send_response(status, headers, @body)
39
+ @body.call(data)
40
+
41
+ finish
42
+ end
43
+
44
+ def render_sse(data, options = {})
45
+ result = "id: #{sse_event_id}\n"
46
+ result << "retry: #{options[:retry]}\n" if options[:retry]
47
+
48
+ data.split(/\n/).each {|d| result << "data: #{d}\n" }
49
+ result << "\n"
50
+
51
+ @body.call(result)
52
+ end
53
+
54
+ # Used by SSE
55
+ def sse_event_id
56
+ @sse_event_id ||= Time.now.to_i
57
+ end
58
+
11
59
  end
12
60
  end
@@ -26,23 +26,29 @@ module Cramp
26
26
 
27
27
  def before_start(n = 0)
28
28
  if callback = self.class.before_start_callbacks[n]
29
- EM.next_tick { send(callback) { before_start(n+1) } }
29
+ callback_wrapper { send(callback) { before_start(n+1) } }
30
30
  else
31
31
  continue
32
32
  end
33
33
  end
34
34
 
35
35
  def on_start
36
+ callback_wrapper { start } if respond_to?(:start)
37
+
36
38
  self.class.on_start_callback.each do |callback|
37
- EM.next_tick { send(callback) }
39
+ callback_wrapper { send(callback) unless @finished }
38
40
  end
39
41
  end
40
42
 
41
43
  def on_finish
42
44
  self.class.on_finish_callbacks.each do |callback|
43
- EM.next_tick { send(callback) }
45
+ callback_wrapper { send(callback) }
44
46
  end
45
47
  end
46
48
 
49
+ def callback_wrapper
50
+ EM.next_tick { yield }
51
+ end
52
+
47
53
  end
48
54
  end
@@ -0,0 +1,34 @@
1
+ module Cramp
2
+ module FiberPool
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ class_attribute :fiber_pool
7
+ end
8
+
9
+ module ClassMethods
10
+ def use_fiber_pool(options = {})
11
+ if RUBY_VERSION < '1.9.1'
12
+ raise "Fibers are supported only for Rubies >= 1.9.1"
13
+ end
14
+
15
+ self.fiber_pool = ::FiberPool.new(options[:size] || 100)
16
+ yield self.fiber_pool if block_given?
17
+ include UsesFiberPool
18
+ end
19
+ end
20
+
21
+ module UsesFiberPool
22
+ # Overrides wrapper methods to run callbacks in a fiber
23
+
24
+ def callback_wrapper
25
+ self.fiber_pool.spawn { yield }
26
+ end
27
+
28
+ def timer_method_wrapper(method)
29
+ self.fiber_pool.spawn { send(method) }
30
+ end
31
+ end
32
+
33
+ end
34
+ end
@@ -12,7 +12,7 @@ module Cramp
12
12
  end
13
13
 
14
14
  def keep_connection_alive
15
- render " "
15
+ @body.call " "
16
16
  end
17
17
 
18
18
  end
@@ -0,0 +1,6 @@
1
+ module Cramp
2
+ # All the usual Cramp::Action stuff. But the request is terminated as soon as render() is called.
3
+ class LongPolling < Action
4
+ self.transport = :long_polling
5
+ end
6
+ end
@@ -19,9 +19,11 @@ module Cramp
19
19
  @timers = []
20
20
  end
21
21
 
22
+ protected
23
+
22
24
  def continue
23
25
  super
24
- EM.next_tick { start_periodic_timers }
26
+ start_periodic_timers
25
27
  end
26
28
 
27
29
  def init_async_body
@@ -33,11 +35,9 @@ module Cramp
33
35
  end
34
36
  end
35
37
 
36
- private
37
-
38
38
  def start_periodic_timers
39
39
  self.class.periodic_timers.each do |method, options|
40
- @timers << EventMachine::PeriodicTimer.new(options[:every] || 1) { send(method) }
40
+ @timers << EventMachine::PeriodicTimer.new(options[:every] || 1) { timer_method_wrapper(method) unless @finished }
41
41
  end
42
42
  end
43
43
 
@@ -45,5 +45,9 @@ module Cramp
45
45
  @timers.each {|t| t.cancel }
46
46
  end
47
47
 
48
+ def timer_method_wrapper(method)
49
+ send(method)
50
+ end
51
+
48
52
  end
49
53
  end
data/lib/cramp/sse.rb ADDED
@@ -0,0 +1,5 @@
1
+ module Cramp
2
+ class SSE < Action
3
+ self.transport = :sse
4
+ end
5
+ end
@@ -14,7 +14,9 @@ module Cramp
14
14
  callback = options.delete(:callback) || block
15
15
  headers = headers.merge('async.callback' => callback)
16
16
 
17
- EM.run { @request.get(path, headers) }
17
+ EM.run do
18
+ catch(:async) { @request.get(path, headers) }
19
+ end
18
20
  end
19
21
 
20
22
  def get_body(path, options = {}, headers = {}, &block)
@@ -22,7 +24,9 @@ module Cramp
22
24
  response_callback = proc {|response| response[-1].each {|chunk| callback.call(chunk) } }
23
25
  headers = headers.merge('async.callback' => response_callback)
24
26
 
25
- EM.run { @request.get(path, headers) }
27
+ EM.run do
28
+ catch(:async) { @request.get(path, headers) }
29
+ end
26
30
  end
27
31
 
28
32
  def get_body_chunks(path, options = {}, headers = {}, &block)
@@ -1,47 +1,4 @@
1
1
  module Cramp
2
- module WebsocketExtension
3
- WEBSOCKET_RECEIVE_CALLBACK = 'websocket.receive_callback'.freeze
4
-
5
- def websocket?
6
- @env['HTTP_CONNECTION'] == 'Upgrade' && @env['HTTP_UPGRADE'] == 'WebSocket'
7
- end
8
-
9
- def websocket_upgrade_data
10
- location = "ws://#{@env['HTTP_HOST']}#{@env['REQUEST_PATH']}"
11
- challenge = solve_challange(
12
- @env['HTTP_SEC_WEBSOCKET_KEY1'],
13
- @env['HTTP_SEC_WEBSOCKET_KEY2'],
14
- @env['rack.input'].read
15
- )
16
-
17
- upgrade = "HTTP/1.1 101 Web Socket Protocol Handshake\r\n"
18
- upgrade << "Upgrade: WebSocket\r\n"
19
- upgrade << "Connection: Upgrade\r\n"
20
- upgrade << "Sec-WebSocket-Origin: #{@env['HTTP_ORIGIN']}\r\n"
21
- upgrade << "Sec-WebSocket-Location: #{location}\r\n\r\n"
22
- upgrade << challenge
23
-
24
- upgrade
25
- end
26
-
27
- def solve_challange(first, second, third)
28
- # Refer to 5.2 4-9 of the draft 76
29
- sum =
30
- [extract_nums(first) / count_spaces(first)].pack("N*") +
31
- [extract_nums(second) / count_spaces(second)].pack("N*") +
32
- third
33
- Digest::MD5.digest(sum)
34
- end
35
-
36
- def extract_nums(string)
37
- string.scan(/[0-9]/).join.to_i
38
- end
39
-
40
- def count_spaces(string)
41
- string.scan(/ /).size
42
- end
43
- end
44
-
45
2
  class Websocket < Abstract
46
3
  include PeriodicTimer
47
4
 
@@ -68,7 +25,8 @@ module Cramp
68
25
  end
69
26
 
70
27
  def render(body)
71
- @body.call("\x00#{body}\xff")
28
+ data = ["\x00", body, "\xFF"].map(&method(:encode)) * ''
29
+ @body.call(data)
72
30
  end
73
31
 
74
32
  def _on_data_receive(data)
@@ -79,6 +37,12 @@ module Cramp
79
37
  end
80
38
  end
81
39
  end
82
-
40
+
41
+ protected
42
+
43
+ def encode(string, encoding = 'UTF-8')
44
+ string.respond_to?(:force_encoding) ? string.force_encoding(encoding) : string
45
+ end
46
+
83
47
  end
84
48
  end
@@ -0,0 +1,82 @@
1
+ module Cramp
2
+ module WebsocketExtension
3
+ WEBSOCKET_RECEIVE_CALLBACK = 'websocket.receive_callback'.freeze
4
+
5
+ def websocket?
6
+ @env['HTTP_CONNECTION'] == 'Upgrade' && @env['HTTP_UPGRADE'] == 'WebSocket'
7
+ end
8
+
9
+ def secure_websocket?
10
+ if @env.has_key?('HTTP_X_FORWARDED_PROTO')
11
+ @env['HTTP_X_FORWARDED_PROTO'] == 'https'
12
+ else
13
+ @env['HTTP_ORIGIN'] =~ /^https:/i
14
+ end
15
+ end
16
+
17
+ def websocket_url
18
+ scheme = secure_websocket? ? 'wss:' : 'ws:'
19
+ @env['websocket.url'] = "#{ scheme }//#{ @env['HTTP_HOST'] }#{ @env['REQUEST_URI'] }"
20
+ end
21
+
22
+ class WebSocketHandler
23
+ def initialize(env, websocket_url, body)
24
+ @env = env
25
+ @websocket_url = websocket_url
26
+ @body = body
27
+ end
28
+ end
29
+
30
+ class Protocol75 < WebSocketHandler
31
+ def handshake
32
+ upgrade = "HTTP/1.1 101 Web Socket Protocol Handshake\r\n"
33
+ upgrade << "Upgrade: WebSocket\r\n"
34
+ upgrade << "Connection: Upgrade\r\n"
35
+ upgrade << "WebSocket-Origin: #{@env['HTTP_ORIGIN']}\r\n"
36
+ upgrade << "WebSocket-Location: #{@websocket_url}\r\n\r\n"
37
+ upgrade
38
+ end
39
+ end
40
+
41
+ class Protocol76 < WebSocketHandler
42
+ def handshake
43
+ key1 = @env['HTTP_SEC_WEBSOCKET_KEY1']
44
+ value1 = number_from_key(key1) / spaces_in_key(key1)
45
+
46
+ key2 = @env['HTTP_SEC_WEBSOCKET_KEY2']
47
+ value2 = number_from_key(key2) / spaces_in_key(key2)
48
+
49
+ hash = Digest::MD5.digest(big_endian(value1) +
50
+ big_endian(value2) +
51
+ @body)
52
+
53
+ upgrade = "HTTP/1.1 101 Web Socket Protocol Handshake\r\n"
54
+ upgrade << "Upgrade: WebSocket\r\n"
55
+ upgrade << "Connection: Upgrade\r\n"
56
+ upgrade << "Sec-WebSocket-Origin: #{@env['HTTP_ORIGIN']}\r\n"
57
+ upgrade << "Sec-WebSocket-Location: #{@websocket_url}\r\n\r\n"
58
+ upgrade << hash
59
+ upgrade
60
+ end
61
+
62
+ private
63
+
64
+ def number_from_key(key)
65
+ key.scan(/[0-9]/).join('').to_i(10)
66
+ end
67
+
68
+ def spaces_in_key(key)
69
+ key.scan(/ /).size
70
+ end
71
+
72
+ def big_endian(number)
73
+ string = ''
74
+ [24,16,8,0].each do |offset|
75
+ string << (number >> offset & 0xFF).chr
76
+ end
77
+ string
78
+ end
79
+ end
80
+
81
+ end
82
+ end
@@ -31,6 +31,17 @@ end
31
31
 
32
32
  class Thin::Request
33
33
  include Cramp::WebsocketExtension
34
+
35
+ def websocket_upgrade_data
36
+ handler = if @env['HTTP_SEC_WEBSOCKET_KEY1'] and @env['HTTP_SEC_WEBSOCKET_KEY2']
37
+ Protocol76
38
+ else
39
+ Protocol75
40
+ end
41
+
42
+ handler.new(@env, websocket_url, body.read).handshake
43
+ end
44
+
34
45
  end
35
46
 
36
47
  class Thin::Response
@@ -0,0 +1,85 @@
1
+ # Author:: Mohammad A. Ali (mailto:oldmoe@gmail.com)
2
+ # Copyright:: Copyright (c) 2008 eSpace, Inc.
3
+ # License:: Distributes under the same terms as Ruby
4
+
5
+ require 'fiber'
6
+
7
+ class Fiber
8
+
9
+ #Attribute Reference--Returns the value of a fiber-local variable, using
10
+ #either a symbol or a string name. If the specified variable does not exist,
11
+ #returns nil.
12
+ def [](key)
13
+ local_fiber_variables[key]
14
+ end
15
+
16
+ #Attribute Assignment--Sets or creates the value of a fiber-local variable,
17
+ #using either a symbol or a string. See also Fiber#[].
18
+ def []=(key,value)
19
+ local_fiber_variables[key] = value
20
+ end
21
+
22
+ private
23
+
24
+ def local_fiber_variables
25
+ @local_fiber_variables ||= {}
26
+ end
27
+ end
28
+
29
+ class FiberPool
30
+
31
+ # gives access to the currently free fibers
32
+ attr_reader :fibers
33
+ attr_reader :busy_fibers
34
+
35
+ # Code can register a proc with this FiberPool to be called
36
+ # every time a Fiber is finished. Good for releasing resources
37
+ # like ActiveRecord database connections.
38
+ attr_accessor :generic_callbacks
39
+
40
+ # Prepare a list of fibers that are able to run different blocks of code
41
+ # every time. Once a fiber is done with its block, it attempts to fetch
42
+ # another one from the queue
43
+ def initialize(count = 100)
44
+ @fibers,@busy_fibers,@queue,@generic_callbacks = [],{},[],[]
45
+ count.times do |i|
46
+ fiber = Fiber.new do |block|
47
+ loop do
48
+ block.call
49
+
50
+ # callbacks are called in a reverse order, much like c++ destructor
51
+ Fiber.current[:callbacks].pop.call while Fiber.current[:callbacks].length > 0
52
+ generic_callbacks.each do |cb|
53
+ cb.call
54
+ end
55
+
56
+ if @queue.any?
57
+ block = @queue.shift
58
+ else
59
+ @busy_fibers.delete(Fiber.current.object_id)
60
+ @fibers.unshift Fiber.current
61
+ block = Fiber.yield
62
+ end
63
+
64
+ end
65
+ end
66
+ fiber[:callbacks] = []
67
+ fiber[:em_keys] = []
68
+ @fibers << fiber
69
+ end
70
+ end
71
+
72
+ # If there is an available fiber use it, otherwise, leave it to linger
73
+ # in a queue
74
+ def spawn(&block)
75
+ if fiber = @fibers.shift
76
+ fiber[:callbacks] = []
77
+ @busy_fibers[fiber.object_id] = fiber
78
+ fiber.resume(block)
79
+ else
80
+ @queue << block
81
+ end
82
+ self # we are keen on hiding our queue
83
+ end
84
+
85
+ end
metadata CHANGED
@@ -1,12 +1,12 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cramp
3
3
  version: !ruby/object:Gem::Version
4
- hash: 19
4
+ hash: 17
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
- - 12
9
- version: "0.12"
8
+ - 13
9
+ version: "0.13"
10
10
  platform: ruby
11
11
  authors:
12
12
  - Pratik Naik
@@ -14,7 +14,7 @@ autorequire:
14
14
  bindir: bin
15
15
  cert_chain: []
16
16
 
17
- date: 2011-02-13 00:00:00 +00:00
17
+ date: 2011-07-31 00:00:00 +01:00
18
18
  default_executable:
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency
@@ -25,12 +25,12 @@ dependencies:
25
25
  requirements:
26
26
  - - ~>
27
27
  - !ruby/object:Gem::Version
28
- hash: 15
28
+ hash: 21
29
29
  segments:
30
30
  - 3
31
31
  - 0
32
- - 4
33
- version: 3.0.4
32
+ - 9
33
+ version: 3.0.9
34
34
  type: :runtime
35
35
  version_requirements: *id001
36
36
  - !ruby/object:Gem::Dependency
@@ -79,14 +79,19 @@ files:
79
79
  - lib/cramp/action.rb
80
80
  - lib/cramp/body.rb
81
81
  - lib/cramp/callbacks.rb
82
+ - lib/cramp/fiber_pool.rb
82
83
  - lib/cramp/keep_connection_alive.rb
84
+ - lib/cramp/long_polling.rb
83
85
  - lib/cramp/periodic_timer.rb
84
86
  - lib/cramp/rendering.rb
87
+ - lib/cramp/sse.rb
85
88
  - lib/cramp/test_case.rb
89
+ - lib/cramp/websocket/extension.rb
86
90
  - lib/cramp/websocket/rainbows_backend.rb
87
91
  - lib/cramp/websocket/thin_backend.rb
88
92
  - lib/cramp/websocket.rb
89
93
  - lib/cramp.rb
94
+ - lib/vendor/fiber_pool.rb
90
95
  has_rdoc: true
91
96
  homepage: http://m.onkey.org
92
97
  licenses: []
@@ -117,7 +122,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
117
122
  requirements: []
118
123
 
119
124
  rubyforge_project:
120
- rubygems_version: 1.5.2
125
+ rubygems_version: 1.6.2
121
126
  signing_key:
122
127
  specification_version: 3
123
128
  summary: Asynchronous web framework.