momentarily 0.0.1 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
data/README.textile ADDED
@@ -0,0 +1,61 @@
1
+ h1. Introducing Momentarily
2
+
3
+ Momentarily was created to allow Rails developers to *speed up their applications for end users*. Momentarily gives developers the ability to quickly and safely *move slow operations into a thread and out of the request chain* so user experience is not impacted by operations like sending emails, updating web services or database operations.
4
+
5
+ Momentarily is a wrapper around EventMachine with Rails considerations baked in. EventMachine offers both an evented-model reactor designed for asychronous IO, and a thread-pool manager and processing queue for blocking IO. Momentarily provides the code to integrate EventMachine with Passenger, Thin, Rails Console or other environments, and then use the thread-pool manager in a Rails-safe manner.
6
+
7
+ *Contributions are welcome!*
8
+
9
+ To use Momentarily, first add it to your Gemfile:
10
+
11
+ gem 'momentarily'
12
+
13
+ You'll then need to start the reactor. Create a *config/initializers/momentarily.rb* file like this:
14
+
15
+ Momentarily.start
16
+
17
+ That's it! Once complete, *Momentarily.reactor_running?* should be true.
18
+
19
+ Momentarily provides the very useful "later" command. Calling .later will spawn a thread to complete the tasks you provide, allowing your Rails requests to complete and return to users. For example:
20
+
21
+ def my_rails_action
22
+ # do something that has to be done inline
23
+ Momentarily.later( Proc.new{
24
+ # stuff you don't need done before you return ot the users, and might take awhile
25
+ # update databases using ActiveRecord
26
+ # send emails with ActionMailer
27
+ # other stuff
28
+ })
29
+ render
30
+ end
31
+
32
+ This will allow your request to complete without waiting for your activities to complete. Note that you are responsible for ensuring that your provided Proc is thread safe.
33
+
34
+ Momentarily.later does the following:
35
+
36
+ * Checks out (and returns) a connection from the ActiveRecord connection pool
37
+ * Sends your request to EventMachine for scheduling
38
+ * Catches and handles any general exceptions in the work thread for debugging
39
+ * Automatically expires any work that fails to return within the default timeout. This is designed to avoid hung threads and eventual thread pool starvation. You can change the default of 60 seconds by setting *Momentarily.timeout* with a new value.
40
+
41
+ For consistency, Momentarily also provide interfaces *EventMachine.next_tick* and *EventMachine.defer* as *Momentarily.next_tick* and *Momentarily.defer*. Use next_tick to schedule non-blocking IO operations, like AMQP calls or Pusher notifications. *Momentarily.defer* operates similarly to *Momentarily.later*, except *Momentarily.later* checks out an ActiveRecord connection, manages timeouts and handles exceptions for better safety in a Rails environment. We use AMQP also, so our momentarily.rb initializer looks like this:
42
+
43
+ require 'amqp'
44
+
45
+ Momentarily.start
46
+
47
+ Momentarily.next_tick( Proc.new {
48
+ AMQP.channel ||= AMQP::Channel.new(AMQP.connect(:host=> Q_SERVER, :user=> Q_USER, :pass => Q_PASS, :vhost => Q_VHOST ))
49
+ } )
50
+
51
+ Momentarily bridges the gap between using non-blocking IO for asynch operations (like EventMachine) and industrial strength queueing (like RabbitMQ and AMQP) to offload work for later execution. Both have their places, but it's not always feasible to use only non-blocking IO, and it's often not worth the trouble to create messages and a consumer just to shave 500ms off a web request. Our goal is to make it simple to defer even small tasks and ensure a snappy end user experience.
52
+
53
+ h2. Other notes:
54
+
55
+ You can enable debug mode by setting *Momentarily.debug = true*. Momentarily will then put messages to the console about operation and halt if an unhandled exception occurs.
56
+
57
+ By default, EventMachine maintains a pool of 20 available threads. You may have to tune this to your processing environment.
58
+
59
+ Similarly, ActiveRecord keeps a pool of connections available, and *the default is too low* - each thread explicitly claims (then releases) one of these connections while working. You can add "pool: 30" or similar to your database.yml file to increase it. Again, you'll have to tune this number to your processing environment.
60
+
61
+ *On running Momentarily with Thin*. Momentarily detects the Thin object in the environment and skips startup of the EventMachine reactor in that case (preferring to wait for Thin to do it.) If for whatever reason you have Thin defined in your gemfile, but aren't using it as an active server, Momentarily will not start EM as intended (unless you are In the Rails console). To make sure your tests work, make sure your thin dependency is defined in the production and/or development group in your Gemfile so it is not loaded when tests are run.
data/lib/momentarily.rb CHANGED
@@ -1,47 +1,86 @@
1
- require "momentarily/version"
1
+ $:.unshift(File.dirname(__FILE__)) unless
2
+ $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
3
+
4
+ require "rubygems"
5
+ require "momentarily/version.rb"
2
6
  require "eventmachine"
3
- require 'amqp'
7
+ require 'timeout'
8
+
9
+ # designed for rails
10
+ require 'active_record'
11
+ require 'active_support/core_ext/module'
4
12
 
5
13
  module Momentarily
6
-
7
- class MomentarilyEM
8
- # this pattern is described here http://rdoc.info/github/ruby-amqp/amqp/master/file/docs/ConnectingToTheBroker.textile#In_Web_applications__Ruby_on_Rails__Sinatra__Merb__Rack_
9
- # and here: http://railstips.org/blog/archives/2011/05/04/eventmachine-and-passenger/
10
- def initialize()
11
- self.start()
12
- end
14
+ mattr_accessor :timeout, :debug
15
+ @@timeout = 60
16
+ @@debug = false
13
17
 
14
- def self.start
15
- if defined?(PhusionPassenger)
16
- PhusionPassenger.on_event(:starting_worker_process) do |forked|
17
- # for passenger, we need to avoid orphaned threads
18
- if forked && EM.reactor_running?
19
- EM.stop
20
- end
21
- Thread.new {
22
- EM.run do
23
- AMQP.channel ||= AMQP::Channel.new(AMQP.connect(:host=> Q_SERVER, :user=> Q_USER, :pass => Q_PASS, :vhost => Q_VHOST ))
24
- end
25
- }
26
- die_gracefully_on_signal
27
- end
28
- else
29
- # faciliates debugging
30
- Thread.abort_on_exception = true
31
- # just spawn a thread and start it up
32
- Thread.new {
33
- EM.run do
34
- AMQP.channel ||= AMQP::Channel.new(AMQP.connect(:host=> Q_SERVER, :user=> Q_USER, :pass => Q_PASS, :vhost => Q_VHOST ))
35
- end
36
- } unless defined?(Thin)
37
- # Thin is built on EventMachine, doesn't need this thread
18
+ def Momentarily.start
19
+ # faciliates debugging
20
+ Thread.abort_on_exception = @@debug
21
+ if defined?(PhusionPassenger)
22
+ puts "Momentarily: Passenger thread init." if @@debug
23
+ PhusionPassenger.on_event(:starting_worker_process) do |forked|
24
+ # for passenger, we need to avoid orphaned threads
25
+ if forked && EM.reactor_running?
26
+ EM.stop
27
+ end
28
+ Thread.new {
29
+ EM.run
30
+ }
31
+ die_gracefully_on_signal
38
32
  end
33
+ # if Thin is defined, but in console mode, Thin probably isn't running
34
+ elsif (!EM.reactor_running? && defined?(Thin).nil?) || !defined?(Rails::Console).nil?
35
+ # spawn a thread and start it up
36
+ puts "Momentarily: Standard thread init." if @@debug
37
+ Thread.new {
38
+ EM.run
39
+ }
40
+ else
41
+ # Thin is built on EventMachine, doesn't need another thread
42
+ puts "Momentarily: Reactor already running or detected Thin." if @@debug
39
43
  end
40
44
 
41
- def self.die_gracefully_on_signal
42
- Signal.trap("INT") { EM.stop }
43
- Signal.trap("TERM") { EM.stop }
45
+ end
46
+
47
+ def Momentarily.die_gracefully_on_signal
48
+ Signal.trap("INT") { EM.stop }
49
+ Signal.trap("TERM") { EM.stop }
50
+ end
51
+
52
+ def Momentarily.later(work = nil, callback = nil, &block)
53
+ EM.defer( self.railsify(( work || block )), self.railsify(callback) )
54
+ end
55
+
56
+ def Momentarily.next_tick(work = nil, &block)
57
+ EM.next_tick( ( work || block ) )
58
+ end
59
+
60
+ def Momentarily.defer(work = nil, callback = nil, &block)
61
+ EM.defer( ( work || block ), callback)
62
+ end
63
+
64
+ def Momentarily.reactor_running?
65
+ EM.reactor_running?
66
+ end
67
+
68
+ private
69
+ def Momentarily.railsify(work)
70
+ unless work.nil?
71
+ Proc.new {
72
+ # checks out connection for use in this thread
73
+ ActiveRecord::Base.connection_pool.with_connection do
74
+ begin
75
+ # make sure it doesn't hang
76
+ Timeout::timeout(@@timeout) { work.call() unless work.nil? }
77
+ rescue => e
78
+ puts "Momentarily: Thread failed with: " + e.to_s if @@debug
79
+ rescue TimeoutError => e
80
+ puts "Momentarily: Thread timeout." if @@debug
81
+ end
82
+ end }
44
83
  end
45
- end # MomentarilyEM
84
+ end
46
85
 
47
86
  end # Momentarily
@@ -1,3 +1,3 @@
1
1
  module Momentarily
2
- VERSION = "0.0.1"
2
+ VERSION = "0.0.3"
3
3
  end
@@ -0,0 +1,90 @@
1
+ require 'test/unit'
2
+ require './lib/momentarily.rb'
3
+
4
+ ActiveRecord::Base.establish_connection(YAML::load(File.read(File.dirname(__FILE__) + '/../config/database.yml')))
5
+
6
+ class TestMomentarily < Test::Unit::TestCase
7
+ def setup
8
+ if !ActiveRecord::Base.connection.table_exists?('momentarily_test')
9
+ puts "Creating test table..."
10
+ AddEntitiesTable.create
11
+ end
12
+ Momentarily.debug = false
13
+ if !Momentarily.reactor_running?
14
+ Momentarily.start
15
+ end
16
+ end
17
+
18
+ def test_instantiation
19
+ assert(EM.reactor_running?)
20
+ end
21
+
22
+ def test_later
23
+ a = 1
24
+ Momentarily.later( Proc.new { a = 2 } )
25
+ sleep(0.5)
26
+ assert(a == 2)
27
+
28
+ Momentarily.later( Proc.new { asdfputs "I am a bad proc" } )
29
+ assert(EM.reactor_running?)
30
+
31
+ a = 1
32
+ Momentarily.timeout = 1
33
+ Momentarily.later( Proc.new {
34
+ sleep(2)
35
+ a = 2 } )
36
+ sleep(3)
37
+ assert(a == 1)
38
+
39
+ end
40
+
41
+ def test_later_blocks
42
+ a = 1
43
+ Momentarily.later { a = 2 }
44
+ sleep(0.5)
45
+ assert(a == 2)
46
+
47
+ Momentarily.later { asdfputs "I am a bad proc" }
48
+ assert(EM.reactor_running?)
49
+
50
+ a = 1
51
+ Momentarily.timeout = 1
52
+ Momentarily.later {
53
+ sleep(2)
54
+ a = 2 }
55
+ sleep(3)
56
+ assert(a == 1)
57
+
58
+ end
59
+
60
+ def test_next_tick
61
+ a = 1
62
+ Momentarily.next_tick( Proc.new { a = 2 })
63
+ sleep(0.5)
64
+ assert(a == 2)
65
+ Momentarily.next_tick { a = 3 }
66
+ sleep(0.5)
67
+ assert(a == 3)
68
+ end
69
+
70
+ def simulate_thin()
71
+ require 'thin'
72
+ Thread.new { EM.run }
73
+ end
74
+ end
75
+
76
+ class AddEntitiesTable < ActiveRecord::Migration
77
+ # up and down functions call broken code in Rail3 migrations gem, called it 'create'
78
+
79
+ def self.create
80
+ create_table "momentarily_test", :force => true do |t|
81
+ t.string "key", :limit => 512, :null => false
82
+ t.string "value"
83
+ t.datetime "created_at"
84
+ t.datetime "updated_at"
85
+ end
86
+
87
+ add_index "momentarily_test", ["key"], :name => "key"
88
+ end
89
+
90
+ end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: momentarily
3
3
  version: !ruby/object:Gem::Version
4
- hash: 29
4
+ hash: 25
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
8
  - 0
9
- - 1
10
- version: 0.0.1
9
+ - 3
10
+ version: 0.0.3
11
11
  platform: ruby
12
12
  authors:
13
13
  - Joshua Siler
@@ -15,9 +15,64 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2012-01-15 00:00:00 Z
19
- dependencies: []
20
-
18
+ date: 2012-01-16 00:00:00 Z
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: thin
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ none: false
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ hash: 3
29
+ segments:
30
+ - 0
31
+ version: "0"
32
+ type: :development
33
+ version_requirements: *id001
34
+ - !ruby/object:Gem::Dependency
35
+ name: eventmachine
36
+ prerelease: false
37
+ requirement: &id002 !ruby/object:Gem::Requirement
38
+ none: false
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ hash: 3
43
+ segments:
44
+ - 0
45
+ version: "0"
46
+ type: :runtime
47
+ version_requirements: *id002
48
+ - !ruby/object:Gem::Dependency
49
+ name: activerecord
50
+ prerelease: false
51
+ requirement: &id003 !ruby/object:Gem::Requirement
52
+ none: false
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ hash: 3
57
+ segments:
58
+ - 0
59
+ version: "0"
60
+ type: :runtime
61
+ version_requirements: *id003
62
+ - !ruby/object:Gem::Dependency
63
+ name: activesupport
64
+ prerelease: false
65
+ requirement: &id004 !ruby/object:Gem::Requirement
66
+ none: false
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ hash: 3
71
+ segments:
72
+ - 0
73
+ version: "0"
74
+ type: :runtime
75
+ version_requirements: *id004
21
76
  description: A rails gem that allows you to briefly defer execution of tasks and return from requests faster.
22
77
  email:
23
78
  - joshua.siler@gmail.com
@@ -28,12 +83,10 @@ extensions: []
28
83
  extra_rdoc_files: []
29
84
 
30
85
  files:
31
- - .gitignore
32
- - Gemfile
33
- - Rakefile
34
86
  - lib/momentarily.rb
35
87
  - lib/momentarily/version.rb
36
- - momentarily.gemspec
88
+ - README.textile
89
+ - test/test_momentarily.rb
37
90
  homepage: ""
38
91
  licenses: []
39
92
 
@@ -67,5 +120,5 @@ rubygems_version: 1.8.10
67
120
  signing_key:
68
121
  specification_version: 3
69
122
  summary: A rails gem that allows you to briefly defer execution of tasks and return from requests faster.
70
- test_files: []
71
-
123
+ test_files:
124
+ - test/test_momentarily.rb
data/.gitignore DELETED
@@ -1,4 +0,0 @@
1
- *.gem
2
- .bundle
3
- Gemfile.lock
4
- pkg/*
data/Gemfile DELETED
@@ -1,4 +0,0 @@
1
- source "http://rubygems.org"
2
-
3
- # Specify your gem's dependencies in momentarily.gemspec
4
- gemspec
data/Rakefile DELETED
@@ -1 +0,0 @@
1
- require "bundler/gem_tasks"
data/momentarily.gemspec DELETED
@@ -1,24 +0,0 @@
1
- # -*- encoding: utf-8 -*-
2
- $:.push File.expand_path("../lib", __FILE__)
3
- require "momentarily/version"
4
-
5
- Gem::Specification.new do |s|
6
- s.name = "momentarily"
7
- s.version = Momentarily::VERSION
8
- s.authors = ["Joshua Siler"]
9
- s.email = ["joshua.siler@gmail.com"]
10
- s.homepage = ""
11
- s.summary = %q{A rails gem that allows you to briefly defer execution of tasks and return from requests faster.}
12
- s.description = %q{A rails gem that allows you to briefly defer execution of tasks and return from requests faster.}
13
-
14
- s.rubyforge_project = "momentarily"
15
-
16
- s.files = `git ls-files`.split("\n")
17
- s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
- s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
- s.require_paths = ["lib"]
20
-
21
- # specify any dependencies here; for example:
22
- # s.add_development_dependency "rspec"
23
- # s.add_runtime_dependency "rest-client"
24
- end