cramp 0.12 → 0.13

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.
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.