revenant 0.0.2

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.
@@ -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: []