revenant 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,56 @@
1
+ # Each lock module must implement a singleton method called +lock_function+
2
+ # This method should return a proc that will be called with a lock_name argument.
3
+ # The proc should return true if the lock has been acquired, false otherwise
4
+ #
5
+ # Lock modules may expose other methods to users as needed, but only
6
+ # +lock_function+ is required.
7
+ # Modules register new lock types by calling Revenant.register(name, klass)
8
+ module Revenant
9
+ module MySQL
10
+ extend self
11
+
12
+ def lock_function
13
+ Proc.new do |lock_name|
14
+ ::Revenant::MySQL.acquire_lock(lock_name)
15
+ end
16
+ end
17
+
18
+ # Expects the connection to behave like an instance of +Mysql+
19
+ # If you need something else, replace +acquire_lock+ with your own code.
20
+ # Or define your own lock_function while configuring a new Revenant task.
21
+ def acquire_lock(lock_name)
22
+ begin
23
+ acquired = false
24
+ sql = lock_query(lock_name)
25
+ connection.query(sql) do |result|
26
+ acquired = result.fetch_row.first == "1"
27
+ end
28
+ acquired
29
+ rescue ::Exception
30
+ false
31
+ end
32
+ end
33
+
34
+ def lock_query(lock_name)
35
+ "select get_lock('#{lock_name}',0);"
36
+ end
37
+
38
+ # Currently defaults to the ActiveRecord connection if AR is loaded.
39
+ # Set this in your task setup block if that is not what you want.
40
+ def connection
41
+ @connection ||= if defined?(ActiveRecord)
42
+ ActiveRecord::Base.connection.raw_connection
43
+ else
44
+ raise "No connection established or discovered. Use Revenant::MySQL::connection="
45
+ end
46
+ end
47
+
48
+ def connection=(conn)
49
+ @connection = conn
50
+ end
51
+ end
52
+
53
+ # This is how you register a new lock_type
54
+ register :mysql, MySQL
55
+ end
56
+
@@ -0,0 +1,115 @@
1
+ require 'revenant/task'
2
+ require 'revenant/pid'
3
+ require 'revenant/manager'
4
+
5
+ # "startup" and "shutdown" are the methods Task expects modules like
6
+ # this one to replace.
7
+ module ::Revenant::Daemon
8
+ # Installs this plugin in the given +task+.
9
+ # Out of the box, this is always to provide daemon support.
10
+ # +install+ is expected to know when to do nothing.
11
+ def self.install(task)
12
+ if task.daemon?
13
+ class << task
14
+ include ::Revenant::Daemon
15
+ end
16
+ end
17
+ end
18
+
19
+ def startup
20
+ @original_dir = ::Revenant.working_directory
21
+ daemonize
22
+ log "#{name} is starting"
23
+ end
24
+
25
+ def shutdown
26
+ @pid.remove
27
+
28
+ if restart_pending?
29
+ log "#{name} is restarting"
30
+ if @original_dir
31
+ Dir.chdir @original_dir
32
+ end
33
+ system script
34
+ else
35
+ log "#{name} is shutting down"
36
+ end
37
+
38
+ exit 0
39
+ end
40
+
41
+ ##
42
+ ## Everything else is a daemon implementation detail.
43
+ ##
44
+
45
+ def pid_file
46
+ @options[:pid_file] ||= File.join("/tmp", "#{@name}.pid")
47
+ end
48
+
49
+ def log_file
50
+ @options[:log_file]
51
+ end
52
+
53
+ def script
54
+ @options[:script] ||= File.expand_path($0)
55
+ end
56
+
57
+ protected
58
+
59
+ def daemonize
60
+ verify_permissions
61
+ ::Revenant::Manager.daemonize(name, log_file)
62
+ @pid.create
63
+ daemon_signals
64
+ end
65
+
66
+ def verify_permissions
67
+ unless File.executable?(script)
68
+ error "script file is not executable: #{script.inspect}"
69
+ exit 1
70
+ end
71
+
72
+ dir = File.dirname(pid_file)
73
+ unless File.directory?(dir) && File.writable?(dir)
74
+ error "pid file is not writeable: #{pid_file.inspect}"
75
+ exit 1
76
+ end
77
+
78
+ @pid = ::Revenant::PID.new(pid_file)
79
+ if @pid.exists?
80
+ error "pid file exists: #{pid_file.inspect}. unclean shutdown?"
81
+ exit 1
82
+ end
83
+
84
+ if log_file && dir = File.dirname(log_file)
85
+ unless File.directory?(dir) && File.writable?(dir)
86
+ error "log file is not writeable: #{log_file.inspect}"
87
+ exit 1
88
+ end
89
+ end
90
+ end
91
+
92
+ def daemon_signals
93
+ trap("TERM") do
94
+ log "Received TERM signal"
95
+ shutdown_soon
96
+ end
97
+
98
+ trap("QUIT") do
99
+ log "QUIT: #{caller.inspect}"
100
+ shutdown_soon
101
+ end
102
+
103
+ trap("USR1") do
104
+ log "TRACE: #{caller.inspect}"
105
+ end
106
+
107
+ trap("USR2") do
108
+ log "Received USR2 signal"
109
+ restart_soon
110
+ end
111
+ end
112
+ end
113
+
114
+ # Register this plugin
115
+ ::Revenant.plugins[:daemon] = ::Revenant::Daemon
data/lib/revenant.rb ADDED
@@ -0,0 +1,72 @@
1
+ module Revenant
2
+ VERSION = "0.0.1"
3
+
4
+ # Register a new type of lock.
5
+ # User code specifies which by setting lock_type = :something
6
+ # while configuring a Revenant::Task
7
+ def self.register(lock_type, klass)
8
+ @lock_types ||= {}
9
+ if klass.respond_to?(:lock_function)
10
+ @lock_types[lock_type.to_sym] = klass
11
+ else
12
+ raise ArgumentError, "#{klass} must have a `lock_function` that returns a callable object"
13
+ end
14
+ end
15
+
16
+ def self.find_module(lock_type)
17
+ @lock_types ||= {}
18
+ @lock_types.fetch(lock_type.to_sym) do
19
+ raise ArgumentError, "unknown lock type: #{lock_type.inspect}"
20
+ end
21
+ end
22
+
23
+ def self.plugins
24
+ @plugins ||= {}
25
+ end
26
+
27
+ def self.init
28
+ require 'revenant/task'
29
+ require_dir "locks"
30
+ require_dir "plugins"
31
+ end
32
+
33
+ # Given 'locks/foo', will require ./locks/foo/*.rb' with normalized
34
+ # (relative) paths. (e.g. require 'locks/foo/example' for example.rb)
35
+ def self.require_dir(relative_path)
36
+ current = File.expand_path('..', __FILE__)
37
+ make_relative = /#{current}\//
38
+ $LOAD_PATH << current unless $LOAD_PATH.include?(current)
39
+ pattern = File.join(current, relative_path, '*.rb')
40
+ Dir[pattern].each do |full_path|
41
+ relative_name = full_path.gsub(make_relative,'').gsub(/\.rb$/,'')
42
+ require relative_name
43
+ end
44
+ end
45
+
46
+ def self.working_directory
47
+ # If the 'PWD' environment variable points to our
48
+ # current working directory, use it instead of Dir.pwd.
49
+ # It may have a better name for the same destination,
50
+ # in the presence of symlinks.
51
+ e = File.stat(env_pwd = ENV['PWD'])
52
+ p = File.stat(Dir.pwd)
53
+ e.ino == p.ino && e.dev == p.dev ? env_pwd : Dir.pwd
54
+ rescue
55
+ Dir.pwd
56
+ end
57
+ end
58
+
59
+ module Kernel
60
+ def revenant(name = nil)
61
+ unless String === name || Symbol === name
62
+ raise ArgumentError, "Usage: task = revenant('example') {|r| configure_as_needed }"
63
+ end
64
+ instance = ::Revenant::Task.new(name)
65
+ instance.daemon = true # daemonized by default if available
66
+ yield instance if block_given?
67
+ instance
68
+ end
69
+ end
70
+
71
+ ::Revenant.init
72
+
@@ -0,0 +1,62 @@
1
+ module Revenant
2
+ module Manager
3
+ extend self
4
+
5
+ def daemonize(name, log_file = nil)
6
+ # Firstly, get rid of the filthy dirty original process.
7
+ exit!(0) if fork
8
+
9
+ # Now that we aren't attached to a terminal, we can become
10
+ # a session leader.
11
+ begin
12
+ Process.setsid
13
+ rescue Errno::EPERM
14
+ raise SystemCallError, "setsid failed. terminal failed to detach?"
15
+ end
16
+ trap 'SIGHUP', 'IGNORE' # don't do anything crazy when this process exits
17
+
18
+ # Finally, time to create a daemonized process
19
+ exit!(0) if fork
20
+
21
+ $0 = name.to_s # set the process name
22
+ close_open_files
23
+ redirect_io_to(log_file)
24
+ srand # re-seed the PRNG with our 'final' pid
25
+ end
26
+
27
+ # Close anything that is not one of the three standard IO streams.
28
+ def close_open_files
29
+ ObjectSpace.each_object(IO) do |io|
30
+ next if [STDIN, STDOUT, STDERR].include?(io)
31
+ begin
32
+ io.close unless io.closed?
33
+ rescue ::Exception
34
+ end
35
+ end
36
+ end
37
+
38
+ # Redirects STDIN, STDOUT, and STDERR to the specified +log_file+
39
+ # or to /dev/null if none is given.
40
+ def redirect_io_to(log_file)
41
+ log_file ||= "/dev/null"
42
+ reopen_io STDIN, "/dev/null"
43
+ reopen_io STDOUT, log_file, "a"
44
+ reopen_io STDERR, STDOUT
45
+ STDERR.sync = STDOUT.sync = true
46
+ end
47
+
48
+ # Attempts to reopen an IO object.
49
+ def reopen_io(io, path, mode = nil)
50
+ begin
51
+ if mode
52
+ io.reopen(path, mode)
53
+ else
54
+ io.reopen(path)
55
+ end
56
+ io.binmode
57
+ rescue ::Exception
58
+ end
59
+ end
60
+ end
61
+ end
62
+
@@ -0,0 +1,24 @@
1
+ module Revenant
2
+ class PID
3
+ def initialize(file)
4
+ @file = file
5
+ end
6
+
7
+ def exists?
8
+ File.exists?(@file)
9
+ end
10
+
11
+ def create
12
+ return false if exists?
13
+
14
+ File.open(@file, 'w') do |f|
15
+ f.write(Process.pid)
16
+ end
17
+ true
18
+ end
19
+
20
+ def remove
21
+ File.unlink(@file) if exists?
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,244 @@
1
+ require 'revenant'
2
+ require 'time'
3
+
4
+ module Revenant
5
+ class Task
6
+ attr_reader :name
7
+ attr_accessor :options
8
+ attr_writer :logger
9
+
10
+ def initialize(name = nil)
11
+ unless String === name || Symbol === name
12
+ raise ArgumentError, "Usage: new(task_name)"
13
+ end
14
+ @name = name.to_sym
15
+ @options = {}
16
+ end
17
+
18
+ # Generally overridden when Revenant::Daemon is included
19
+ def startup
20
+ log "#{name} is starting"
21
+ trap("INT") { shutdown_soon }
22
+ end
23
+
24
+ ## Generally overridden when Revenant::Daemon is included
25
+ # The stack gets deeper here on every restart; this is here
26
+ # largely to ease testing.
27
+ # Implement your own plugin providing +shutdown+ if you want to
28
+ # make something serious that calls this code after a
29
+ # restart signal.
30
+ def shutdown
31
+ if restart_pending? && @work
32
+ log "#{name} is restarting"
33
+ run(&@work)
34
+ else
35
+ log "#{name} is shutting down"
36
+ end
37
+ end
38
+
39
+ # Takes actual block of code that is to be guarded by
40
+ # the lock. The +run_loop+ method does the actual work.
41
+ #
42
+ # If 'daemon?' is true, your code (including +on_load+)
43
+ # will execute after a fork.
44
+ #
45
+ # Make sure you don't open files and sockets in the exiting
46
+ # parent process by mistake. Open them in code that is called
47
+ # via +on_load+.
48
+ def run(&block)
49
+ unless @work = block
50
+ raise ArgumentError, "Usage: run { while_we_have_the_lock }"
51
+ end
52
+ @shutdown = false
53
+ @restart = false
54
+ install_plugins
55
+ startup # typically daemonizes the process, can have various implementations
56
+ on_load.call(self) if on_load
57
+ run_loop(&@work)
58
+ on_exit.call(self) if on_exit
59
+ shutdown
60
+ end
61
+
62
+ # Code to run just before the task looks for a lock
63
+ # This code runs after any necessary forks, and is
64
+ # therefore the proper place to open databases, logfiles,
65
+ # and any other resources you require.
66
+ def on_load(&block)
67
+ @on_load ||= block
68
+ end
69
+
70
+ # Code to run when the task is exiting.
71
+ def on_exit(&block)
72
+ @on_exit ||= block
73
+ end
74
+
75
+ # Used to pick the Task's +lock_module+
76
+ # Particular lock types may offer various helpful features
77
+ # via this lock module.
78
+ # Defaults to :mysql
79
+ def lock_type
80
+ @lock_type ||= :mysql
81
+ end
82
+
83
+ # Set a new lock type for this Task.
84
+ def lock_type=(val)
85
+ @lock_type = val.to_sym
86
+ end
87
+
88
+ # Set your own lock function. Will be called with a lock name as the arg.
89
+ # Should return true if a lock has been acquired, false otherwise.
90
+ # task.lock_function {|name| # .. }
91
+ def lock_function(&block)
92
+ if block_given?
93
+ @lock_function = block
94
+ else
95
+ @lock_function ||= lock_module.lock_function
96
+ end
97
+ end
98
+
99
+ # Returns a module that knows how to do some distributed locking.
100
+ # May not be the code that actually performs the lock, if this
101
+ # Task has had a +lock_function+ assigned to it explicitly.
102
+ def lock_module
103
+ ::Revenant.find_module(lock_type)
104
+ end
105
+
106
+ # How many work loops to perform before re-acquiring the lock.
107
+ # Defaults to 5.
108
+ # Setting it to 0 or nil will assume the lock is forever valid after
109
+ # acquisition.
110
+ def relock_every
111
+ @relock_every ||= 5
112
+ end
113
+
114
+ # Set the frequency with which locks are re-acquired.
115
+ # Setting it to 0 or nil will assume the lock is forever valid after
116
+ # acquisition.
117
+ def relock_every=(loops)
118
+ loops ||= 0
119
+ if Integer === loops && loops >= 0
120
+ @relock_every = loops
121
+ else
122
+ raise ArgumentError, "argument must be nil or an integer >= 0"
123
+ end
124
+ end
125
+
126
+ # How many seconds to sleep after each work loop.
127
+ # When we don't have the lock, how long to sleep before checking again.
128
+ # Default is 5 seconds.
129
+ def sleep_for
130
+ @sleep_for ||= 5
131
+ end
132
+
133
+ # Set the number of seconds to sleep for after a work loop.
134
+ def sleep_for=(seconds)
135
+ seconds ||= 0
136
+ if Integer === seconds && seconds >= 0
137
+ @sleep_for = seconds
138
+ else
139
+ raise ArgumentError, "argument must be nil or an integer >= 0"
140
+ end
141
+ end
142
+
143
+ # This could be the moment.
144
+ def shutdown_pending?
145
+ @shutdown ||= false
146
+ end
147
+
148
+ # At last, back to war.
149
+ def restart_pending?
150
+ @restart ||= false
151
+ end
152
+
153
+ # Task will restart at the earliest safe opportunity after
154
+ # +restart_soon+ is called.
155
+ def restart_soon
156
+ @restart = true
157
+ @shutdown = true
158
+ end
159
+
160
+ # Task will shut down at the earliest safe opportunity after
161
+ # +shutdown_soon+ is called.
162
+ def shutdown_soon
163
+ @restart = false
164
+ @shutdown = true
165
+ end
166
+
167
+ ## Used to lazily store/retrieve options that may be needed by plugins.
168
+ # We may want to capture, say, +log_file+ before actually loading the
169
+ # code that might care about such a concept.
170
+ def method_missing(name, *args)
171
+ name = name.to_s
172
+ last_char = name[-1,1]
173
+ super(name, *args) unless last_char == "=" || last_char == "?"
174
+ attr_name = name[0..-2].to_sym # :foo for 'foo=' or 'foo?'
175
+ if last_char == "="
176
+ @options[attr_name] = args.at(0)
177
+ else
178
+ @options[attr_name]
179
+ end
180
+ end
181
+
182
+ def log(message)
183
+ logger.puts "[#{$$}] #{Time.now.iso8601(2)} - #{message}"
184
+ end
185
+
186
+ def error(message)
187
+ logger.puts "[#{$$}] #{Time.now.iso8601(2)} - ERROR: #{message}"
188
+ end
189
+
190
+ def logger
191
+ @logger ||= STDERR
192
+ end
193
+
194
+ # Install any plugins that have registered themselves, or a custom
195
+ # list if the user has set it themselves.
196
+ def install_plugins
197
+ ::Revenant.plugins.each do |name, plugin|
198
+ plugin.install(self)
199
+ end
200
+ end
201
+
202
+ # Run until we receive a shutdown/reload signal,
203
+ # or when the worker raises an Interrupt.
204
+ # Runs after a fork when Revenant::Daemon is enabled.
205
+ def run_loop(&block)
206
+ acquired = false
207
+ begin
208
+ until shutdown_pending?
209
+ # The usual situation
210
+ if relock_every != 0
211
+ i ||= 0
212
+ # 0 % anything is 0, so we always try to get the lock on the first loop.
213
+ if (i %= relock_every) == 0
214
+ acquired = lock_function.call(@name)
215
+ end
216
+ else
217
+ # With relock_every set to 0, only acquire the lock once.
218
+ # Hope you're sure that lock beongs to you.
219
+ acquired ||= lock_function.call(@name)
220
+ i = 0 # no point in incrementing something we don't check.
221
+ end
222
+
223
+ yield if acquired
224
+
225
+ # Sleep one second at a time so we can quickly respond to
226
+ # shutdown requests.
227
+ sleep_for.times do
228
+ sleep(1) unless shutdown_pending?
229
+ end
230
+ i += 1
231
+ end # loop
232
+ rescue ::Interrupt => ex
233
+ log "shutting down after interrupt: #{ex.class} - #{ex.message}"
234
+ shutdown_soon # Always shut down from an Interrupt, even mid-restart.
235
+ return
236
+ rescue ::Exception => ex
237
+ error "restarting after error: #{ex.class} - #{ex.message}"
238
+ error "backtrace: #{ex.backtrace.join("\n")}"
239
+ restart_soon # Restart if we run into an exception.
240
+ end # begin block
241
+ end # run_loop
242
+ end # Task
243
+ end # Revenant
244
+
metadata ADDED
@@ -0,0 +1,50 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: revenant
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Wilson Bilkovich
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2010-06-06 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description: A framework for building reliable distributed workers.
15
+ email: wilson@supremetyrant.com
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - lib/locks/mysql.rb
21
+ - lib/plugins/daemon.rb
22
+ - lib/revenant/manager.rb
23
+ - lib/revenant/pid.rb
24
+ - lib/revenant/task.rb
25
+ - lib/revenant.rb
26
+ homepage: http://github.com/wilson/revenant
27
+ licenses: []
28
+ post_install_message:
29
+ rdoc_options: []
30
+ require_paths:
31
+ - lib
32
+ required_ruby_version: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ required_rubygems_version: !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: 1.3.7
44
+ requirements: []
45
+ rubyforge_project:
46
+ rubygems_version: 1.8.24
47
+ signing_key:
48
+ specification_version: 2
49
+ summary: Distributed daemons that just will not die.
50
+ test_files: []