fire_and_forget 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "http://rubygems.org"
2
+
3
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,34 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ fire_and_forget (0.1.0)
5
+ daemons (~> 1.1.0)
6
+ json (~> 1.4.6)
7
+
8
+ GEM
9
+ remote: http://rubygems.org/
10
+ specs:
11
+ daemons (1.1.0)
12
+ git (1.2.5)
13
+ jeweler (1.5.1)
14
+ bundler (~> 1.0.0)
15
+ git (>= 1.2.5)
16
+ rake
17
+ jnunemaker-matchy (0.4.0)
18
+ json (1.4.6)
19
+ rake (0.8.7)
20
+ rr (1.0.2)
21
+ shoulda (2.11.3)
22
+
23
+ PLATFORMS
24
+ ruby
25
+
26
+ DEPENDENCIES
27
+ bundler (~> 1.0.0)
28
+ daemons (~> 1.1.0)
29
+ fire_and_forget!
30
+ jeweler (~> 1.5.1)
31
+ jnunemaker-matchy (~> 0.4)
32
+ json (~> 1.4.6)
33
+ rr (~> 1.0.2)
34
+ shoulda
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 Garry Hill
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,105 @@
1
+ = Fire & Forget
2
+
3
+ Fire & Forget is a simple, framework agnostic, background task launcher for Ruby web apps.
4
+
5
+ What it does:
6
+
7
+ - Designed for infrequent calls to very long running operations
8
+ - Provides a mechanism for calling any external script from within a HTTP request
9
+ - Gives simple inter-process communication for status updates
10
+ - Launches long running tasks as completely independent processes so that you can restart/redeploy your application without interrupting any running tasks
11
+ - Provides a 'kill' mechanism so you can cancel your tasks
12
+
13
+ What it doesn't:
14
+
15
+ - Have any kind of queue mechanism. If you fire the same task twice then that task will be running twice
16
+ - Have any persistant state
17
+
18
+ == Quick Start
19
+
20
+
21
+ FAF works by running a simple TCP socket server so before we want to use it we have to start this server.
22
+
23
+ $ faf
24
+ FAF process 16235 listening on 127.0.0.1:3001...
25
+
26
+ The service defaults to listening to port 3001 on localhost. You can change these values thus:
27
+
28
+ $ faf -a 192.168.1.22 -p 9090
29
+
30
+ If you do this you will need to set both the web-app and the tasks to use the right values using
31
+
32
+ FireAndForget.port = 9090
33
+ FireAndForget.bind_address = "192.168.1.22"
34
+
35
+ Now inside our Ruby app:
36
+
37
+ require 'rubygems'
38
+ require 'fire_and_forget'
39
+
40
+ # First set register our task by giving it a name and a path to a script file
41
+ FireAndForget.add_task(:long_running_task, "/path/to/script")
42
+
43
+ # when we want to call the script we simple call #fire passing the name of the task and the options
44
+ # we want to pass
45
+ FireAndForget.fire(:long_running_task, {:param3 => "value3"})
46
+
47
+ # Or, use the name as a method:
48
+ FireAndForget.long_running_task({:param3 => "value3"})
49
+
50
+ This will result in the following command being exec'd in an independent process:
51
+
52
+ /path/to/script --param3="value3"
53
+
54
+ It up to the script to parse and deal with the command line options.
55
+
56
+ Interprocess communication is relatively easy. Take the following as the source of the script "/path/to/script"
57
+
58
+ #!/usr/bin/env ruby
59
+
60
+ require 'rubygems'
61
+ require 'fire_and_forget'
62
+
63
+ # this will mix in the comms methods and map this task to the :long_running_task label
64
+ # used in the calling script
65
+ include FireAndForget::Daemon[:long_running_task]
66
+
67
+ 30.times do |i|
68
+ # update our status. What you put here is up to you, but it should be a String
69
+ set_task_status("#{i+1} of 30")
70
+ sleep(1)
71
+ end
72
+
73
+ Now in the client all we have to do to get the status for our task is:
74
+
75
+ FireAndForget.get_status(:long_running_task)
76
+ # => "18 of 30"
77
+
78
+ If we decided we've had enough then we can kill it:
79
+
80
+ # Send "SIG_TERM" to our task
81
+ FireAndForget.term(:long_running_task)
82
+
83
+ # Or send any signal (see the Process.kill documentation)
84
+ FireAndForget.kill(:long_running_task, "HUP")
85
+
86
+ == Security
87
+
88
+ F&F is intended to be run in a relatively trusted environment and by default only binds to localhost to stop external access. But to stop unintended access to system commands, only scripts belonging to the same user as the F&F server process are executed. This might not be enough so in the future I might add a limit on the locations of runnable scripts.
89
+
90
+
91
+ == Contributing to fire_and_forget
92
+
93
+ * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
94
+ * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
95
+ * Fork the project
96
+ * Start a feature/bugfix branch
97
+ * Commit and push until you are happy with your contribution
98
+ * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
99
+ * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
100
+
101
+ == Copyright
102
+
103
+ Copyright (c) 2010 Garry Hill. See LICENSE.txt for
104
+ further details.
105
+
data/Rakefile ADDED
@@ -0,0 +1,62 @@
1
+ require 'rubygems'
2
+ # require 'bundler'
3
+ # begin
4
+ # Bundler.setup(:default, :development)
5
+ # rescue Bundler::BundlerError => e
6
+ # $stderr.puts e.message
7
+ # $stderr.puts "Run `bundle install` to install missing gems"
8
+ # exit e.status_code
9
+ # end
10
+ require 'rake'
11
+ require 'jeweler'
12
+
13
+ require 'lib/fire_and_forget/version'
14
+
15
+ Jeweler::Tasks.new do |gem|
16
+ # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
17
+ gem.name = "fire_and_forget"
18
+ gem.homepage = "http://github.com/magnetised/fire_and_forget"
19
+ gem.license = "MIT"
20
+ gem.summary = %Q{Fire & Forget is a simple, framework agnostic, background task launcher for Ruby web apps}
21
+ gem.description = %Q{Fire & Forget is a simple, framework agnostic, background task launcher for Ruby web apps}
22
+ gem.email = "garry@magnetised.info"
23
+ gem.authors = ["Garry Hill"]
24
+ gem.version = FireAndForget::VERSION
25
+
26
+ gem.add_runtime_dependency "json", "~>1.4.6"
27
+ gem.add_runtime_dependency "daemons", "~>1.1.0"
28
+
29
+ gem.add_development_dependency "shoulda", ">= 0"
30
+ gem.add_development_dependency 'jnunemaker-matchy', '~>0.4'
31
+ gem.add_development_dependency "bundler", "~> 1.0.0"
32
+ gem.add_development_dependency "jeweler", "~> 1.5.1"
33
+ gem.add_development_dependency "rr", "~>1.0.2"
34
+ end
35
+
36
+ Jeweler::RubygemsDotOrgTasks.new
37
+
38
+ require 'rake/testtask'
39
+ Rake::TestTask.new(:test) do |test|
40
+ test.libs << 'lib' << 'test'
41
+ test.pattern = 'test/**/test_*.rb'
42
+ test.verbose = true
43
+ end
44
+
45
+ require 'rcov/rcovtask'
46
+ Rcov::RcovTask.new do |test|
47
+ test.libs << 'test'
48
+ test.pattern = 'test/**/test_*.rb'
49
+ test.verbose = true
50
+ end
51
+
52
+ task :default => :test
53
+
54
+ require 'rake/rdoctask'
55
+ Rake::RDocTask.new do |rdoc|
56
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
57
+
58
+ rdoc.rdoc_dir = 'rdoc'
59
+ rdoc.title = "fire_and_forget #{version}"
60
+ rdoc.rdoc_files.include('README*')
61
+ rdoc.rdoc_files.include('lib/**/*.rb')
62
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.0
data/bin/fire_forget ADDED
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env ruby
2
+
3
+
4
+ Dir.chdir(File.join(File.dirname(__FILE__), '..'))
5
+
6
+ faf_lib_path = File.expand_path(File.dirname(__FILE__) + "/../lib")
7
+ $:.unshift(faf_lib_path) unless $:.include?(faf_lib_path)
8
+
9
+ require 'rubygems' unless defined?(Gem)
10
+
11
+ require 'ostruct'
12
+ require 'optparse'
13
+ require 'socket'
14
+ require 'fire_and_forget'
15
+
16
+ options = OpenStruct.new
17
+ options.bind_address = "127.0.0.1"
18
+ options.port = FAF::DEFAULT_PORT
19
+
20
+ OptionParser.new do |opts|
21
+ opts.on("-a", "--bind-address ADDRESS", "Bind Address") { |v| options.bind_address = v }
22
+ opts.on("-p", "--port PORT", Integer, "Port") { |p| options.port = p }
23
+ end.parse!
24
+
25
+ server = nil
26
+
27
+ begin
28
+ server = TCPServer.new(options.bind_address, options.port)
29
+ rescue Errno::EADDRINUSE
30
+ puts "FAF unable to bind to #{options.bind_address}:#{options.port}"
31
+ exit(1)
32
+ end
33
+
34
+ run = true
35
+
36
+ server_thread = Thread.new do
37
+ while run and (session = server.accept)
38
+ request = response = ""
39
+
40
+ while l = session.gets
41
+ request << l
42
+ end
43
+ session.close_read
44
+
45
+ begin
46
+ response = FAF::Server.parse(request)
47
+ rescue => e
48
+ response = "ERROR #{e}"
49
+ end
50
+ session.write(response)
51
+ session.close
52
+ end
53
+ end
54
+
55
+ ['INT', 'TERM'].each do |signal|
56
+ trap(signal) do
57
+ run = false;
58
+ server_thread.exit
59
+ end
60
+ end
61
+
62
+ puts "Fire&Forget process #{$$} listening on #{options.bind_address}:#{options.port}..."
63
+
64
+ server_thread.join
65
+
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ Dir.chdir(File.join(File.dirname(__FILE__), '..'))
4
+
5
+ $:.unshift(File.dirname(__FILE__) + "/../lib")
6
+
7
+ require 'rubygems'
8
+ require 'bundler'
9
+
10
+ begin
11
+ Bundler.setup(:default)
12
+ rescue Bundler::BundlerError => e
13
+ $stderr.puts e.message
14
+ $stderr.puts "Run `bundle install` to install missing gems"
15
+ exit e.status_code
16
+ end
17
+
18
+ require 'fire_and_forget'
19
+
20
+ include FAF::Daemon[:publish]
21
+
22
+
23
+ File.open(File.join(File.dirname(__FILE__), "../../long.out"), 'w') do |file|
24
+ file.sync = true
25
+ file.write("PID: #{$$}\n")
26
+ file.write(`ps -xO nice | grep '^#{$$}'`)
27
+ file.write("\n")
28
+ 60.times do |i|
29
+ file.write("#{i}\n")
30
+ begin
31
+ set_status(i)
32
+ rescue Exception => e
33
+ file.write(e.to_s + "\n")
34
+ end
35
+ sleep(1)
36
+ end
37
+ end
@@ -0,0 +1,114 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{fire_and_forget}
8
+ s.version = "0.1.0"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Garry Hill"]
12
+ s.date = %q{2010-12-15}
13
+ s.default_executable = %q{fire_forget}
14
+ s.description = %q{Fire & Forget is a simple, framework agnostic, background task launcher for Ruby web apps}
15
+ s.email = %q{garry@magnetised.info}
16
+ s.executables = ["fire_forget"]
17
+ s.extra_rdoc_files = [
18
+ "LICENSE.txt",
19
+ "README.rdoc"
20
+ ]
21
+ s.files = [
22
+ "Gemfile",
23
+ "Gemfile.lock",
24
+ "LICENSE.txt",
25
+ "README.rdoc",
26
+ "Rakefile",
27
+ "VERSION",
28
+ "bin/fire_forget",
29
+ "examples/long_task",
30
+ "fire_and_forget.gemspec",
31
+ "lib/fire_and_forget.rb",
32
+ "lib/fire_and_forget/client.rb",
33
+ "lib/fire_and_forget/command.rb",
34
+ "lib/fire_and_forget/command/fire.rb",
35
+ "lib/fire_and_forget/command/get_status.rb",
36
+ "lib/fire_and_forget/command/kill.rb",
37
+ "lib/fire_and_forget/command/set_pid.rb",
38
+ "lib/fire_and_forget/command/set_status.rb",
39
+ "lib/fire_and_forget/config.rb",
40
+ "lib/fire_and_forget/daemon.rb",
41
+ "lib/fire_and_forget/launcher.rb",
42
+ "lib/fire_and_forget/server.rb",
43
+ "lib/fire_and_forget/task.rb",
44
+ "lib/fire_and_forget/utilities.rb",
45
+ "lib/fire_and_forget/version.rb",
46
+ "test/helper.rb",
47
+ "test/test_fire_and_forget.rb"
48
+ ]
49
+ s.homepage = %q{http://github.com/magnetised/fire_and_forget}
50
+ s.licenses = ["MIT"]
51
+ s.require_paths = ["lib"]
52
+ s.rubygems_version = %q{1.3.6}
53
+ s.summary = %q{Fire & Forget is a simple, framework agnostic, background task launcher for Ruby web apps}
54
+ s.test_files = [
55
+ "test/helper.rb",
56
+ "test/test_fire_and_forget.rb"
57
+ ]
58
+
59
+ if s.respond_to? :specification_version then
60
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
61
+ s.specification_version = 3
62
+
63
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
64
+ s.add_runtime_dependency(%q<fire_and_forget>, [">= 0"])
65
+ s.add_runtime_dependency(%q<json>, ["~> 1.4.6"])
66
+ s.add_runtime_dependency(%q<daemons>, ["~> 1.1.0"])
67
+ s.add_development_dependency(%q<shoulda>, [">= 0"])
68
+ s.add_development_dependency(%q<jnunemaker-matchy>, ["~> 0.4"])
69
+ s.add_development_dependency(%q<bundler>, ["~> 1.0.0"])
70
+ s.add_development_dependency(%q<jeweler>, ["~> 1.5.1"])
71
+ s.add_development_dependency(%q<rr>, ["~> 1.0.2"])
72
+ s.add_runtime_dependency(%q<json>, ["~> 1.4.6"])
73
+ s.add_runtime_dependency(%q<daemons>, ["~> 1.1.0"])
74
+ s.add_development_dependency(%q<shoulda>, [">= 0"])
75
+ s.add_development_dependency(%q<jnunemaker-matchy>, ["~> 0.4"])
76
+ s.add_development_dependency(%q<bundler>, ["~> 1.0.0"])
77
+ s.add_development_dependency(%q<jeweler>, ["~> 1.5.1"])
78
+ s.add_development_dependency(%q<rr>, ["~> 1.0.2"])
79
+ else
80
+ s.add_dependency(%q<fire_and_forget>, [">= 0"])
81
+ s.add_dependency(%q<json>, ["~> 1.4.6"])
82
+ s.add_dependency(%q<daemons>, ["~> 1.1.0"])
83
+ s.add_dependency(%q<shoulda>, [">= 0"])
84
+ s.add_dependency(%q<jnunemaker-matchy>, ["~> 0.4"])
85
+ s.add_dependency(%q<bundler>, ["~> 1.0.0"])
86
+ s.add_dependency(%q<jeweler>, ["~> 1.5.1"])
87
+ s.add_dependency(%q<rr>, ["~> 1.0.2"])
88
+ s.add_dependency(%q<json>, ["~> 1.4.6"])
89
+ s.add_dependency(%q<daemons>, ["~> 1.1.0"])
90
+ s.add_dependency(%q<shoulda>, [">= 0"])
91
+ s.add_dependency(%q<jnunemaker-matchy>, ["~> 0.4"])
92
+ s.add_dependency(%q<bundler>, ["~> 1.0.0"])
93
+ s.add_dependency(%q<jeweler>, ["~> 1.5.1"])
94
+ s.add_dependency(%q<rr>, ["~> 1.0.2"])
95
+ end
96
+ else
97
+ s.add_dependency(%q<fire_and_forget>, [">= 0"])
98
+ s.add_dependency(%q<json>, ["~> 1.4.6"])
99
+ s.add_dependency(%q<daemons>, ["~> 1.1.0"])
100
+ s.add_dependency(%q<shoulda>, [">= 0"])
101
+ s.add_dependency(%q<jnunemaker-matchy>, ["~> 0.4"])
102
+ s.add_dependency(%q<bundler>, ["~> 1.0.0"])
103
+ s.add_dependency(%q<jeweler>, ["~> 1.5.1"])
104
+ s.add_dependency(%q<rr>, ["~> 1.0.2"])
105
+ s.add_dependency(%q<json>, ["~> 1.4.6"])
106
+ s.add_dependency(%q<daemons>, ["~> 1.1.0"])
107
+ s.add_dependency(%q<shoulda>, [">= 0"])
108
+ s.add_dependency(%q<jnunemaker-matchy>, ["~> 0.4"])
109
+ s.add_dependency(%q<bundler>, ["~> 1.0.0"])
110
+ s.add_dependency(%q<jeweler>, ["~> 1.5.1"])
111
+ s.add_dependency(%q<rr>, ["~> 1.0.2"])
112
+ end
113
+ end
114
+
@@ -0,0 +1,19 @@
1
+
2
+ module FireAndForget
3
+ DEFAULT_PORT = 3001
4
+
5
+ autoload :Config, "fire_and_forget/config"
6
+ autoload :Utilities, "fire_and_forget/utilities"
7
+ autoload :Launcher, "fire_and_forget/launcher"
8
+ autoload :Task, "fire_and_forget/task"
9
+ autoload :Client, "fire_and_forget/client"
10
+ autoload :Command, "fire_and_forget/command"
11
+ autoload :Server, "fire_and_forget/server"
12
+ autoload :Daemon, "fire_and_forget/daemon"
13
+
14
+ extend Config
15
+ extend Utilities
16
+ extend Launcher
17
+ end
18
+
19
+ FAF = FireAndForget unless defined?(FAF)
@@ -0,0 +1,29 @@
1
+ require 'socket'
2
+
3
+ module FireAndForget
4
+ class Client
5
+
6
+ class << self
7
+ def run(cmd)
8
+ result = open_connection do |connection|
9
+ connection.send(cmd.dump, 0)
10
+ end
11
+ end
12
+
13
+ def open_connection
14
+ connection = result = nil
15
+ begin
16
+ connection = TCPSocket.open(FireAndForget.bind_address, FireAndForget.port)
17
+ yield(connection)
18
+ connection.flush
19
+ connection.close_write
20
+ result = connection.read
21
+ ensure
22
+ connection.close if connection rescue nil
23
+ end
24
+ result
25
+ end
26
+ end
27
+ end
28
+ end
29
+
@@ -0,0 +1,43 @@
1
+
2
+ module FireAndForget
3
+ module Command
4
+ SEPARATOR = "||".freeze
5
+
6
+ def self.load(command)
7
+ Marshal.load(command)
8
+ end
9
+
10
+ class CommandBase
11
+ attr_reader :tag, :cmd, :params, :task
12
+
13
+ def initialize(task, params={})
14
+ @task, @params = task, merge_params(task.params, params)
15
+ end
16
+
17
+ def dump
18
+ Marshal.dump(self)
19
+ end
20
+
21
+ def run
22
+ # overridden in subclasses
23
+ end
24
+
25
+
26
+ def merge_params(task_params, call_params)
27
+ params = task_params.to_a.inject({}) do |hash, (key, value)|
28
+ hash[key.to_s] = value; hash
29
+ end
30
+ call_params.each do |key, value|
31
+ params[key.to_s] = value
32
+ end if call_params
33
+ params
34
+ end
35
+ end
36
+
37
+ autoload :Fire, "fire_and_forget/command/fire"
38
+ autoload :Kill, "fire_and_forget/command/kill"
39
+ autoload :SetStatus, "fire_and_forget/command/set_status"
40
+ autoload :GetStatus, "fire_and_forget/command/get_status"
41
+ autoload :SetPid, "fire_and_forget/command/set_pid"
42
+ end
43
+ end
@@ -0,0 +1,49 @@
1
+
2
+ require 'daemons'
3
+
4
+ module FireAndForget
5
+ module Command
6
+ class Fire < CommandBase
7
+ attr_reader :niceness
8
+
9
+ def niceness
10
+ @task.niceness
11
+ end
12
+
13
+ def binary
14
+ @task.binary
15
+ end
16
+
17
+ def cmd
18
+ %(#{binary} #{FireAndForget.to_arguments(@params)})
19
+ end
20
+
21
+ def valid?
22
+ exists? && permitted?
23
+ end
24
+
25
+ def permitted?
26
+ raise Errno::EACCES.new("'#{binary}' does not belong to user '#{ENV["USER"]}'") unless File.owned?(binary)
27
+ true
28
+ end
29
+
30
+ def exists?
31
+ raise Errno::ENOENT.new("'#{binary}'") unless File.exists?(binary)
32
+ true
33
+ end
34
+
35
+ def run
36
+ if valid?
37
+ pid = fork do
38
+ Daemons.daemonize(:backtrace => true)
39
+ Process.setpriority(Process::PRIO_PROCESS, 0, niceness) if niceness > 0
40
+ exec(cmd)
41
+ end
42
+ Process.detach(pid) if pid
43
+ pid
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+
@@ -0,0 +1,16 @@
1
+ module FireAndForget
2
+ module Command
3
+ class GetStatus < CommandBase
4
+
5
+ def initialize(task_name)
6
+ @task_name = task_name.to_sym
7
+ end
8
+
9
+ def run
10
+ FireAndForget::Server.status[@task_name]
11
+ end
12
+ end
13
+ end
14
+ end
15
+
16
+
@@ -0,0 +1,17 @@
1
+ module FireAndForget
2
+ module Command
3
+ class Kill < CommandBase
4
+
5
+ def initialize(task_name, signal="TERM")
6
+ @task_name, @signal = task_name.to_sym, signal
7
+ end
8
+
9
+ def run
10
+ FireAndForget::Server.kill(@task_name, @signal)
11
+ end
12
+ end
13
+ end
14
+ end
15
+
16
+
17
+
@@ -0,0 +1,19 @@
1
+ module FireAndForget
2
+ module Command
3
+ class SetPid < CommandBase
4
+
5
+ attr_reader :task_name, :pid
6
+
7
+ def initialize(task_name, pid)
8
+ @task_name, @pid = task_name.to_sym, pid.to_i
9
+ end
10
+
11
+ def run
12
+ FireAndForget::Server.pids[@task_name] = @pid
13
+ end
14
+ end
15
+ end
16
+ end
17
+
18
+
19
+
@@ -0,0 +1,17 @@
1
+ module FireAndForget
2
+ module Command
3
+ class SetStatus < CommandBase
4
+
5
+ def initialize(task_name, status_value)
6
+ @task_name, @status_value = task_name.to_sym, status_value
7
+ @pid = $$
8
+ end
9
+
10
+ def run
11
+ FireAndForget::Server.set_pid(@task_name, @pid)
12
+ FireAndForget::Server.status[@task_name] = @status_value
13
+ end
14
+ end
15
+ end
16
+ end
17
+
@@ -0,0 +1,15 @@
1
+ module FireAndForget
2
+ module Config
3
+ attr_accessor :port
4
+ attr_accessor :bind_address
5
+
6
+ def port
7
+ @port ||= FireAndForget::DEFAULT_PORT
8
+ end
9
+
10
+ def bind_address
11
+ @bind_address ||= "127.0.0.1"
12
+ end
13
+ end
14
+ end
15
+
@@ -0,0 +1,31 @@
1
+ module FireAndForget
2
+ module Daemon # need better name!
3
+
4
+ def self.[](task_name)
5
+ m = Module.new do
6
+ def self.included(klass)
7
+ FireAndForget.map_pid(self.task_name, $$)
8
+ rescue Errno::ECONNREFUSED
9
+ # server isn't running but we don't want this to stop our script
10
+ end
11
+
12
+ def self.task_name=(task_name)
13
+ @@task_name = task_name
14
+ end
15
+
16
+ def self.task_name
17
+ @@task_name
18
+ end
19
+
20
+ def set_task_status(status)
21
+ FireAndForget.set_status(@@task_name, status)
22
+ rescue Errno::ECONNREFUSED
23
+ # server isn't running but we don't want this to stop our script
24
+ end
25
+ end
26
+ m.task_name = task_name
27
+ m
28
+ end
29
+
30
+ end
31
+ end
@@ -0,0 +1,67 @@
1
+ module FireAndForget
2
+ module Launcher
3
+ def add_task(task_name, path_to_binary, default_params={}, niceness=0)
4
+ if default_params.is_a?(Numeric)
5
+ niceness = default_params
6
+ default_params = {}
7
+ end
8
+ tasks[task_name] = Task.new(task_name, path_to_binary, default_params, niceness)
9
+ end
10
+
11
+ def binary(task_name)
12
+ tasks[task_name].binary
13
+ end
14
+
15
+ def [](task_name)
16
+ tasks[task_name]
17
+ end
18
+
19
+ def tasks
20
+ @tasks ||= {}
21
+ end
22
+
23
+ def fire(task_name, params={})
24
+ task = tasks[task_name]
25
+ command = Command::Fire.new(task, params)
26
+ Client.run(command)
27
+ end
28
+
29
+ def set_status(task_name, status)
30
+ command = Command::SetStatus.new(task_name, status)
31
+ Client.run(command)
32
+ end
33
+
34
+ def get_status(task_name)
35
+ command = Command::GetStatus.new(task_name)
36
+ Client.run(command)
37
+ end
38
+
39
+ def map_pid(task_name, pid)
40
+ command = Command::SetPid.new(task_name, pid)
41
+ Client.run(command)
42
+ end
43
+
44
+ def term(task_name)
45
+ kill(task_name, "TERM")
46
+ end
47
+
48
+ def int(task_name)
49
+ kill(task_name, "INT")
50
+ end
51
+
52
+ def kill(task_name, signal="TERM")
53
+ command = Command::Kill.new(task_name, signal)
54
+ Client.run(command)
55
+ end
56
+
57
+ protected
58
+
59
+ def method_missing(method, *args, &block)
60
+ if tasks.key?(method)
61
+ fire(method, *args, &block)
62
+ else
63
+ super
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,34 @@
1
+
2
+ module FireAndForget
3
+ class Server
4
+ def self.parse(command_string)
5
+ command = Command.load(command_string)
6
+ run(command)
7
+ end
8
+
9
+ def self.run(cmd)
10
+ cmd.run
11
+ end
12
+
13
+ def self.kill(task_name, signal="TERM")
14
+ pid = pids[task_name]
15
+ Process.kill(signal, pid) unless pid == 0
16
+ end
17
+
18
+ def self.status
19
+ @status ||= {}
20
+ end
21
+
22
+ def self.set_pid(task_name, pid)
23
+ pids[task_name] = pid.to_i
24
+ end
25
+
26
+ def self.get_pid(task)
27
+ pids[task.name]
28
+ end
29
+
30
+ def self.pids
31
+ @pids ||= {}
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,11 @@
1
+
2
+
3
+ module FireAndForget
4
+ class Task
5
+ attr_reader :name, :binary, :niceness, :params
6
+
7
+ def initialize(name, path_to_binary, default_parameters={}, niceness=0)
8
+ @name, @binary, @params, @niceness = name, path_to_binary, default_parameters, niceness
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,20 @@
1
+ require "json"
2
+
3
+ module FireAndForget
4
+ module Utilities
5
+ def to_arguments(params={})
6
+ params.keys.sort { |a, b| a.to_s <=> b.to_s }.map do |key|
7
+ %(--#{key}=#{to_json(params[key])})
8
+ end.join(" ")
9
+ end
10
+
11
+ def to_json(obj)
12
+ if obj.is_a?(String)
13
+ obj.inspect
14
+ else
15
+ JSON.generate(obj)
16
+ end
17
+ end
18
+ end
19
+ end
20
+
@@ -0,0 +1,4 @@
1
+
2
+ module FireAndForget
3
+ VERSION = "0.1.0"
4
+ end
data/test/helper.rb ADDED
@@ -0,0 +1,24 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ begin
4
+ Bundler.setup(:default, :development)
5
+ rescue Bundler::BundlerError => e
6
+ $stderr.puts e.message
7
+ $stderr.puts "Run `bundle install` to install missing gems"
8
+ exit e.status_code
9
+ end
10
+ require 'test/unit'
11
+ require 'shoulda'
12
+ require 'matchy'
13
+ require 'rr'
14
+
15
+
16
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
17
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
18
+
19
+
20
+ require 'fire_and_forget'
21
+
22
+ class Test::Unit::TestCase
23
+ include RR::Adapters::TestUnit
24
+ end
@@ -0,0 +1,131 @@
1
+ require 'helper'
2
+
3
+ class TestFireAndForget < Test::Unit::TestCase
4
+ context "Utilities" do
5
+ should "translate a hash to command line arguments" do
6
+ FAF.to_arguments({
7
+ :param1 => "value1",
8
+ :param2 => "value2",
9
+ :array => [1, 2, "3"]
10
+ }).should == %(--array=[1,2,"3"] --param1="value1" --param2="value2")
11
+ end
12
+ end
13
+ context "configuration" do
14
+ should "enable mapping of task to a binary" do
15
+ FAF.add_task(:publish, "/path/to/binary")
16
+ FAF.binary(:publish).should == "/path/to/binary"
17
+ FAF[:publish].binary.should == "/path/to/binary"
18
+ end
19
+
20
+ should "enable setting of a niceness value for the task" do
21
+ FAF.add_task(:publish, "/path/to/binary", 10)
22
+ FAF[:publish].niceness.should == 10
23
+ end
24
+
25
+ should "enable launching a task by its name" do
26
+ FAF.add_task(:publish, "/path/to/binary")
27
+ args = {:param1 => "param1", :param2 => "param2"}
28
+ mock(FAF).fire(:publish, args)
29
+ FAF.publish(args)
30
+ end
31
+
32
+ should "enable setting of port for server" do
33
+ FAF.port = 3007
34
+ FAF.port.should == 3007
35
+ end
36
+
37
+ should "enable setting an address for the server" do
38
+ FAF.bind_address = "10.0.1.10"
39
+ FAF.bind_address.should == "10.0.1.10"
40
+ end
41
+ end
42
+
43
+ context "commands" do
44
+ should "serialize and deserialize correctly" do
45
+ task = FAF::Task.new(:publish, "/publish", {:param1 => "value1", :param2 => "value2"}, 9)
46
+ cmd = FAF::Command::CommandBase.new(task, {"param2" => "newvalue2", :param3 => "value3"})
47
+ cmd2 = FAF::Command.load(cmd.dump)
48
+ task2 = cmd2.task
49
+ task2.binary.should == task.binary
50
+ task2.params.should == task.params
51
+ task2.name.should == task.name
52
+ cmd.params.should == {"param1" => "value1", "param2" => "newvalue2", "param3" => "value3"}
53
+ end
54
+ end
55
+
56
+ context "actions" do
57
+ setup do
58
+ @task = FAF::Task.new(:publish, "/publish", {:param1 => "value1", :param2 => "value2"}, 9)
59
+ end
60
+ should "set status for a task" do
61
+ cmd = FAF::Command::SetStatus.new(:publish, :doing)
62
+ FAF::Server.run(cmd)
63
+ cmd = FAF::Command::GetStatus.new(:publish)
64
+ status = FAF::Server.run(cmd)
65
+ status.should == :doing
66
+ end
67
+ should "only run scripts belonging to the same user as the ruby process" do
68
+ stub(File).exist?("/publish") { true }
69
+ stub(File).exists?("/publish") { true }
70
+ stub(File).owned?("/publish") { false }
71
+ cmd = FAF::Command::Fire.new(@task)
72
+ lambda { cmd.run }.should raise_error(Errno::EACCES)
73
+ end
74
+ should "give error if binary doesn't exist" do
75
+ stub(File).exist?("/publish") { false }
76
+ stub(File).exists?("/publish") { false }
77
+ stub(File).owned?("/publish") { true }
78
+ cmd = FAF::Command::Fire.new(@task)
79
+ lambda { cmd.run }.should raise_error(Errno::ENOENT )
80
+ end
81
+ end
82
+
83
+ context "client" do
84
+ should "send right command to server" do
85
+ task = FAF.add_task(:publish, "/publish", {:param1 => "value1", :param2 => "value2"}, 12)
86
+ command = Object.new
87
+ mock(command).dump { "dumpedcommand" }
88
+ mock(FAF::Command::Fire).new(task, {:param2 => "value3"}) { command }
89
+ connection = Object.new
90
+ mock(connection).send("dumpedcommand", 0)
91
+ stub(connection).flush
92
+ stub(connection).close_write
93
+ mock(connection).read { "99999" }
94
+ mock(connection).close
95
+ FAF.bind_address = "10.0.1.10"
96
+ FAF.port = 9007
97
+ mock(TCPSocket).open("10.0.1.10", 9007) { connection }
98
+ pid = FAF.publish({:param2 => "value3"})
99
+ pid.should == "99999"
100
+ end
101
+ end
102
+
103
+ context "server" do
104
+ setup do
105
+ @server = FAF::Server.new
106
+ end
107
+
108
+ should "run any command sent to it" do
109
+ command = Object.new
110
+ mock(FAF::Command).load(is_a(String)) { command }
111
+ mock(command).run { "666" }
112
+ result = FAF::Server.parse("object")
113
+ result.should == "666"
114
+ end
115
+ end
116
+ context "daemon methods" do
117
+ setup do
118
+ class ::TaskClass; end
119
+ end
120
+ teardown do
121
+ Object.send(:remove_const, :TaskClass) rescue nil
122
+ end
123
+
124
+ should "map a taskname to a pid when included" do
125
+ mock(FAF::Client).run(satisfy { |cmd|
126
+ (cmd.pid == $$) && (cmd.task_name == :tasking)
127
+ })
128
+ TaskClass.send(:include, FAF::Daemon[:tasking])
129
+ end
130
+ end
131
+ end
metadata ADDED
@@ -0,0 +1,290 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fire_and_forget
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 1
8
+ - 0
9
+ version: 0.1.0
10
+ platform: ruby
11
+ authors:
12
+ - Garry Hill
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2010-12-15 00:00:00 +00:00
18
+ default_executable: fire_forget
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ version_requirements: &id001 !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ segments:
26
+ - 0
27
+ version: "0"
28
+ name: fire_and_forget
29
+ requirement: *id001
30
+ prerelease: false
31
+ type: :runtime
32
+ - !ruby/object:Gem::Dependency
33
+ version_requirements: &id002 !ruby/object:Gem::Requirement
34
+ requirements:
35
+ - - ~>
36
+ - !ruby/object:Gem::Version
37
+ segments:
38
+ - 1
39
+ - 4
40
+ - 6
41
+ version: 1.4.6
42
+ name: json
43
+ requirement: *id002
44
+ prerelease: false
45
+ type: :runtime
46
+ - !ruby/object:Gem::Dependency
47
+ version_requirements: &id003 !ruby/object:Gem::Requirement
48
+ requirements:
49
+ - - ~>
50
+ - !ruby/object:Gem::Version
51
+ segments:
52
+ - 1
53
+ - 1
54
+ - 0
55
+ version: 1.1.0
56
+ name: daemons
57
+ requirement: *id003
58
+ prerelease: false
59
+ type: :runtime
60
+ - !ruby/object:Gem::Dependency
61
+ version_requirements: &id004 !ruby/object:Gem::Requirement
62
+ requirements:
63
+ - - ">="
64
+ - !ruby/object:Gem::Version
65
+ segments:
66
+ - 0
67
+ version: "0"
68
+ name: shoulda
69
+ requirement: *id004
70
+ prerelease: false
71
+ type: :development
72
+ - !ruby/object:Gem::Dependency
73
+ version_requirements: &id005 !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - ~>
76
+ - !ruby/object:Gem::Version
77
+ segments:
78
+ - 0
79
+ - 4
80
+ version: "0.4"
81
+ name: jnunemaker-matchy
82
+ requirement: *id005
83
+ prerelease: false
84
+ type: :development
85
+ - !ruby/object:Gem::Dependency
86
+ version_requirements: &id006 !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - ~>
89
+ - !ruby/object:Gem::Version
90
+ segments:
91
+ - 1
92
+ - 0
93
+ - 0
94
+ version: 1.0.0
95
+ name: bundler
96
+ requirement: *id006
97
+ prerelease: false
98
+ type: :development
99
+ - !ruby/object:Gem::Dependency
100
+ version_requirements: &id007 !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - ~>
103
+ - !ruby/object:Gem::Version
104
+ segments:
105
+ - 1
106
+ - 5
107
+ - 1
108
+ version: 1.5.1
109
+ name: jeweler
110
+ requirement: *id007
111
+ prerelease: false
112
+ type: :development
113
+ - !ruby/object:Gem::Dependency
114
+ version_requirements: &id008 !ruby/object:Gem::Requirement
115
+ requirements:
116
+ - - ~>
117
+ - !ruby/object:Gem::Version
118
+ segments:
119
+ - 1
120
+ - 0
121
+ - 2
122
+ version: 1.0.2
123
+ name: rr
124
+ requirement: *id008
125
+ prerelease: false
126
+ type: :development
127
+ - !ruby/object:Gem::Dependency
128
+ version_requirements: &id009 !ruby/object:Gem::Requirement
129
+ requirements:
130
+ - - ~>
131
+ - !ruby/object:Gem::Version
132
+ segments:
133
+ - 1
134
+ - 4
135
+ - 6
136
+ version: 1.4.6
137
+ name: json
138
+ requirement: *id009
139
+ prerelease: false
140
+ type: :runtime
141
+ - !ruby/object:Gem::Dependency
142
+ version_requirements: &id010 !ruby/object:Gem::Requirement
143
+ requirements:
144
+ - - ~>
145
+ - !ruby/object:Gem::Version
146
+ segments:
147
+ - 1
148
+ - 1
149
+ - 0
150
+ version: 1.1.0
151
+ name: daemons
152
+ requirement: *id010
153
+ prerelease: false
154
+ type: :runtime
155
+ - !ruby/object:Gem::Dependency
156
+ version_requirements: &id011 !ruby/object:Gem::Requirement
157
+ requirements:
158
+ - - ">="
159
+ - !ruby/object:Gem::Version
160
+ segments:
161
+ - 0
162
+ version: "0"
163
+ name: shoulda
164
+ requirement: *id011
165
+ prerelease: false
166
+ type: :development
167
+ - !ruby/object:Gem::Dependency
168
+ version_requirements: &id012 !ruby/object:Gem::Requirement
169
+ requirements:
170
+ - - ~>
171
+ - !ruby/object:Gem::Version
172
+ segments:
173
+ - 0
174
+ - 4
175
+ version: "0.4"
176
+ name: jnunemaker-matchy
177
+ requirement: *id012
178
+ prerelease: false
179
+ type: :development
180
+ - !ruby/object:Gem::Dependency
181
+ version_requirements: &id013 !ruby/object:Gem::Requirement
182
+ requirements:
183
+ - - ~>
184
+ - !ruby/object:Gem::Version
185
+ segments:
186
+ - 1
187
+ - 0
188
+ - 0
189
+ version: 1.0.0
190
+ name: bundler
191
+ requirement: *id013
192
+ prerelease: false
193
+ type: :development
194
+ - !ruby/object:Gem::Dependency
195
+ version_requirements: &id014 !ruby/object:Gem::Requirement
196
+ requirements:
197
+ - - ~>
198
+ - !ruby/object:Gem::Version
199
+ segments:
200
+ - 1
201
+ - 5
202
+ - 1
203
+ version: 1.5.1
204
+ name: jeweler
205
+ requirement: *id014
206
+ prerelease: false
207
+ type: :development
208
+ - !ruby/object:Gem::Dependency
209
+ version_requirements: &id015 !ruby/object:Gem::Requirement
210
+ requirements:
211
+ - - ~>
212
+ - !ruby/object:Gem::Version
213
+ segments:
214
+ - 1
215
+ - 0
216
+ - 2
217
+ version: 1.0.2
218
+ name: rr
219
+ requirement: *id015
220
+ prerelease: false
221
+ type: :development
222
+ description: Fire & Forget is a simple, framework agnostic, background task launcher for Ruby web apps
223
+ email: garry@magnetised.info
224
+ executables:
225
+ - fire_forget
226
+ extensions: []
227
+
228
+ extra_rdoc_files:
229
+ - LICENSE.txt
230
+ - README.rdoc
231
+ files:
232
+ - Gemfile
233
+ - Gemfile.lock
234
+ - LICENSE.txt
235
+ - README.rdoc
236
+ - Rakefile
237
+ - VERSION
238
+ - bin/fire_forget
239
+ - examples/long_task
240
+ - fire_and_forget.gemspec
241
+ - lib/fire_and_forget.rb
242
+ - lib/fire_and_forget/client.rb
243
+ - lib/fire_and_forget/command.rb
244
+ - lib/fire_and_forget/command/fire.rb
245
+ - lib/fire_and_forget/command/get_status.rb
246
+ - lib/fire_and_forget/command/kill.rb
247
+ - lib/fire_and_forget/command/set_pid.rb
248
+ - lib/fire_and_forget/command/set_status.rb
249
+ - lib/fire_and_forget/config.rb
250
+ - lib/fire_and_forget/daemon.rb
251
+ - lib/fire_and_forget/launcher.rb
252
+ - lib/fire_and_forget/server.rb
253
+ - lib/fire_and_forget/task.rb
254
+ - lib/fire_and_forget/utilities.rb
255
+ - lib/fire_and_forget/version.rb
256
+ - test/helper.rb
257
+ - test/test_fire_and_forget.rb
258
+ has_rdoc: true
259
+ homepage: http://github.com/magnetised/fire_and_forget
260
+ licenses:
261
+ - MIT
262
+ post_install_message:
263
+ rdoc_options: []
264
+
265
+ require_paths:
266
+ - lib
267
+ required_ruby_version: !ruby/object:Gem::Requirement
268
+ requirements:
269
+ - - ">="
270
+ - !ruby/object:Gem::Version
271
+ segments:
272
+ - 0
273
+ version: "0"
274
+ required_rubygems_version: !ruby/object:Gem::Requirement
275
+ requirements:
276
+ - - ">="
277
+ - !ruby/object:Gem::Version
278
+ segments:
279
+ - 0
280
+ version: "0"
281
+ requirements: []
282
+
283
+ rubyforge_project:
284
+ rubygems_version: 1.3.6
285
+ signing_key:
286
+ specification_version: 3
287
+ summary: Fire & Forget is a simple, framework agnostic, background task launcher for Ruby web apps
288
+ test_files:
289
+ - test/helper.rb
290
+ - test/test_fire_and_forget.rb