acts_as_service 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ # Copyright (c) 2010 Umamibud, Inc.
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,78 @@
1
+ = acts_as_service
2
+
3
+ == Overview
4
+
5
+ A gem that gives you an easy way to make service-like code and spend all your
6
+ effort on actually writing the code to get your job done.
7
+
8
+ Limitations:
9
+ * one process at a time
10
+ * only been tested while running inside a Rails application
11
+ (e.g. script/runner MySrv.start). should work if you just specify a pidfile
12
+ explicitly, but needs a little testing
13
+ * smooth shutdown depends on your work method returning frequently (however
14
+ long you run without returning from perform_work_chunk is how long it might
15
+ take for the service to shut down
16
+
17
+ Allows you to:
18
+ * start, stop, restart your process
19
+ * ensures that only one of each process is running on a given machine (if this
20
+ is not desired, you can probably hack it or enhance to allow up to N
21
+ instances if you want
22
+ * implement a method you want called over and over, and when the service is
23
+ supposed to shut down, it stops calling the method and exits
24
+ * see what the PID is of the process by inspecting the pidfile
25
+ * explicitly shutdown the process from within the service class (but you'll
26
+ still have to return from perform_work_chunk).
27
+ * initiate shutdown of service externally by either calling MyService.stop or
28
+ adding 'shutdown' to the PID file.
29
+ * hooks to execute methods after start or before stop
30
+
31
+ == What to do
32
+
33
+ require 'acts_as_service' # or add it to your Rails env as a config.gem
34
+
35
+ class MyService
36
+
37
+ acts_as_service
38
+
39
+ def self.perform_work_chunk
40
+ # do stuff that returns in a short period (e.g. a few seconds or less)
41
+ end
42
+
43
+ end
44
+
45
+ you can get fancier and specify your process identifier and how long to sleep
46
+ between calls to perform_work_chunk:
47
+
48
+ class MyService
49
+
50
+ acts_as_service
51
+
52
+ def self.service_name
53
+ "MyAwesomeService"
54
+ end
55
+
56
+ def self.sleep_time
57
+ 5 # seconds
58
+ end
59
+
60
+ def self.perform_work_chunk
61
+ # do stuff that returns in a short period (e.g. a few seconds or less)
62
+ end
63
+
64
+ end
65
+
66
+ == Maintaining acts_as_service
67
+
68
+ Make sure you have jeweler.
69
+
70
+ To build:
71
+
72
+ cd .../acts_as_service
73
+ rake gemspec
74
+ rake build
75
+
76
+ To test:
77
+
78
+ Sorry, little lazy here. Create a sample service class and letter rip!
data/TODO ADDED
@@ -0,0 +1,9 @@
1
+ TODOs
2
+ -----
3
+
4
+ put things in here that could be worked on
5
+
6
+ * extricate the gem from needing to run within Rails (maybe nothing to do here,
7
+ maybe just coming up with different pidfile default path if RAILS_ROOT
8
+ isn't defined. but including class can always just specify explicit path and
9
+ should be fine
@@ -0,0 +1,46 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{acts_as_service}
8
+ s.version = "0.0.2"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Brian Percival"]
12
+ s.date = %q{2010-02-18}
13
+ s.description = %q{A gem with a mixin to let you turn a class into something that runs like a service,
14
+ which you can MyService.start, MyService.stop, and MyService.restart. It tracks
15
+ its own pid. For now, pretty sure it requires that the class is running inside
16
+ a rails context (e.g. run by script/runner MyService.start), but that could
17
+ probably be changed without too much difficulty.
18
+ }
19
+ s.email = %q{percivalatumamibuddotcom}
20
+ s.extra_rdoc_files = [
21
+ "LICENSE",
22
+ "README.rdoc",
23
+ "TODO"
24
+ ]
25
+ s.files = [
26
+ "README.rdoc",
27
+ "acts_as_service.gemspec",
28
+ "lib/acts_as_service.rb"
29
+ ]
30
+ s.homepage = %q{http://github.com/bmpercy/acts_as_service}
31
+ s.rdoc_options = ["--charset=UTF-8"]
32
+ s.require_paths = ["lib"]
33
+ s.rubygems_version = %q{1.3.5}
34
+ s.summary = %q{Makes it very easy to create a service-like class that's easy to start and stop, taken from work at discovereads.com}
35
+
36
+ if s.respond_to? :specification_version then
37
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
38
+ s.specification_version = 3
39
+
40
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
41
+ else
42
+ end
43
+ else
44
+ end
45
+ end
46
+
@@ -0,0 +1,306 @@
1
+ # module that makes it easy to turn a class into a service, giving it methods
2
+ # like stop, start, restart for easy cronjobbage, etc.
3
+ #
4
+ # simply require 'acts_as_service' in your class' file and then call
5
+ # acts_as_service in your class definition, then implement a few required
6
+ # and option methods:
7
+ # - required:
8
+ # - self.perform_work_chunk: do a chunk of work that the service is intended
9
+ # to do. note that this method should return
10
+ # periodically to enable shutdown. alternatively,
11
+ # your method /could/ (but not recommended) check
12
+ # the status to see if it's set to SHUTTING_DOWN
13
+ # and return if so. otherwise, the shutdown
14
+ # by this module will not work.
15
+ #
16
+ # - optional:
17
+ # - self.service_name: return the string name of the service.
18
+ # default is the class name (without module prefixes)
19
+ # - self.service_pid_filename: path to the file that should contain the PID for
20
+ # the process while it's running.
21
+ # default is #{RAILS_ROOT}/tmp/pids/<underscore version of service name>
22
+ # - self.sleep_time: seconds to sleep between calls to peform_work_chunk
23
+ # default: no sleep till brooklyn!
24
+ # - self.after_start: a hook to run a method after the service is started
25
+ # but before first call to perform_work_chunk
26
+ # - self.before_stop: a hook to run a method before final shutdown (and
27
+ # after last run to perform_work_chunk)
28
+ #
29
+ # you can also call self.shutdown() from within your perform_work_chunk
30
+ # code to initiate a shutdown (don't just exit() because there's pidfile
31
+ # cleanup etc. to peform).
32
+ #
33
+ # # my_service.rb:
34
+ #
35
+ # require 'acts_as_service'
36
+ #
37
+ # class MyService
38
+ #
39
+ # acts_as_service
40
+ #
41
+ # # ... define methods
42
+ #
43
+ # end
44
+ #-------------------------------------------------------------------------------
45
+ module ActsAsService
46
+
47
+ ACTS_AS_SERVICE_RUNNING = 'running'
48
+ ACTS_AS_SERVICE_OTHER_RUNNING = 'other running'
49
+ ACTS_AS_SERVICE_STOPPED = 'stopped'
50
+ ACTS_AS_SERVICE_SHUTTING_DOWN = 'shutting down'
51
+ ACTS_AS_SERVICE_PID_NO_PROCESS = 'pid no process'
52
+
53
+ def self.included(base)
54
+ base.extend ClassMethods
55
+ end
56
+
57
+ module ClassMethods
58
+ # starts the process if it's not already running
59
+ #---------------------------------------------------------------------------
60
+ def start
61
+ begin
62
+ if _status != ACTS_AS_SERVICE_STOPPED &&
63
+ _status != ACTS_AS_SERVICE_PID_NO_PROCESS
64
+
65
+ puts "#{_display_name} (#{_pid_file_pid}) is already running. Ignoring."
66
+ else
67
+ # clean out a stale pid
68
+ if _status == ACTS_AS_SERVICE_PID_NO_PROCESS
69
+ puts 'Pid file exists but process is not running. Removing old pid file.'
70
+ File.delete(_pid_filename)
71
+ end
72
+ puts "Starting #{_display_name} (#{_pid})...."
73
+ puts "Run #{name}.stop to stop\n\n"
74
+ File.open(_pid_filename, 'w') {|f| f.write(_pid.to_s) }
75
+ if self.respond_to?(:after_start)
76
+ after_start
77
+ end
78
+ while (_status == ACTS_AS_SERVICE_RUNNING)
79
+ # only sleep if asked to
80
+ if self.respond_to?(:sleep_time)
81
+ sleep sleep_time
82
+ end
83
+ perform_work_chunk
84
+ end
85
+ puts "Shutting down #{_display_name} (#{_pid})"
86
+ File.delete(_pid_filename)
87
+ end
88
+ # if something happens, dump an error and clean up the pidfile if
89
+ # it's owned by this process
90
+ rescue Object => e
91
+ puts "ERROR: #{e}\n#{e.respond_to?(:backtrace) ? e.backtrace.join("\n ") : ''}"
92
+ puts "Exiting (#{_pid})\n"
93
+ if _process_running?
94
+ File.delete(_pid_filename)
95
+ end
96
+ end
97
+ end
98
+
99
+
100
+ # stops the process if it's running
101
+ #---------------------------------------------------------------------------
102
+ def stop
103
+ if _status == ACTS_AS_SERVICE_STOPPED
104
+ puts "#{_display_name} is not running"
105
+ elsif _status == ACTS_AS_SERVICE_PID_NO_PROCESS
106
+ puts 'Pid file exists but process is not running. Removing old pid file.'
107
+ File.delete(_pid_filename)
108
+ else
109
+ pid_to_stop = _pid_file_pid
110
+ puts "Stopping #{_display_name} (#{pid_to_stop})...."
111
+ shutdown
112
+ while (_status != ACTS_AS_SERVICE_STOPPED)
113
+ sleep(1)
114
+ end
115
+ puts "#{_display_name} (#{pid_to_stop}) stopped\n"
116
+ end
117
+ end
118
+
119
+
120
+ # stops the current service process and runs a new one in the current process
121
+ #-----------------------------------------------------------------------------
122
+ def restart
123
+ stop
124
+ start
125
+ end
126
+
127
+
128
+ # initiate shutdown. call this from within perform_work_chunk if the service's
129
+ # work is done and it should shut down (makes sense for cronjobs, say)
130
+ #-----------------------------------------------------------------------------
131
+ def shutdown
132
+ if self.respond_to?(:before_stop)
133
+ before_stop
134
+ end
135
+ # change the pid file so the original process sees this 'stop' signal
136
+ File.open(_pid_filename, 'a') do |f|
137
+ f.write("\n#{ACTS_AS_SERVICE_SHUTTING_DOWN}")
138
+ end
139
+ end
140
+
141
+
142
+ # method for outside consumption of status info. hopefully not
143
+ # tempting method name for class-writers to conflict with...
144
+ #---------------------------------------------------------------------------
145
+ def service_running?
146
+ _status == ACTS_AS_SERVICE_RUNNING || _status == ACTS_AS_SERVICE_OTHER_RUNNING
147
+ end
148
+
149
+
150
+ # method for outside consumption of the pid value. hopefully not
151
+ # tempting method name for class-writers to conflict with...
152
+ #---------------------------------------------------------------------------
153
+ def service_pid
154
+ _pid_file_pid
155
+ end
156
+
157
+
158
+ #---------------------------------------------------------------------------
159
+ # helper methods, no real need to access these from client code
160
+ #---------------------------------------------------------------------------
161
+
162
+
163
+ # fetches the service's name, using class name as default
164
+ #---------------------------------------------------------------------------
165
+ def _display_name
166
+ @@_display_name ||= (respond_to?(:service_name) ?
167
+ service_name :
168
+ name.split("::").last)
169
+ end
170
+
171
+
172
+ # returns the pid filename
173
+ #---------------------------------------------------------------------------
174
+ def _pid_filename
175
+ if defined?(@@_pid_filename)
176
+ return @@_pid_filename
177
+ end
178
+
179
+ if respond_to?(:service_pid_filename)
180
+ @@_pid_filename = service_pid_filename
181
+ else
182
+ @@_pid_filename = File.join(RAILS_ROOT, 'tmp', 'pids',
183
+ "#{_display_name.underscore.gsub(/\s+/, '_')}.pid")
184
+ end
185
+ return @@_pid_filename
186
+ end
187
+
188
+
189
+ # the current status of the service. possible values:
190
+ # ACTS_AS_SERVICE_STOPPED : no service process is running
191
+ # ACTS_AS_SERVICE_SHUTTING_DOWN : process currently shutting down
192
+ # ACTS_AS_SERVICE_RUNNING : the current process is the service process
193
+ # ACTS_AS_SERVICE_OTHER_RUNNING : another pid is running the service
194
+ # ACTS_AS_SERVICE_PID_NO_PROCESS : pidfile exists, but no process running
195
+ #-----------------------------------------------------------------------------
196
+ def _status
197
+ _status = nil
198
+
199
+ # logic:
200
+ # if the pid file doesn't exist, it's stopped
201
+ # otherwise, if 'shutting down', it's shutting down
202
+ # or if the pidfile's pid is running, another process is running
203
+ # or if the pidfile's pid matches this process, it's running
204
+ # otherwise, the pidfile's there but no one's running
205
+ if !_pid_file_exists?
206
+ _status = ACTS_AS_SERVICE_STOPPED
207
+ elsif Regexp.new(ACTS_AS_SERVICE_SHUTTING_DOWN) =~ _pid_file_content
208
+ _status = ACTS_AS_SERVICE_SHUTTING_DOWN
209
+ elsif _process_running?
210
+ _status = ACTS_AS_SERVICE_RUNNING
211
+ elsif _pid_file_process_running?
212
+ _status = ACTS_AS_SERVICE_OTHER_RUNNING
213
+ else
214
+ _status = ACTS_AS_SERVICE_PID_NO_PROCESS
215
+ end
216
+
217
+ return _status
218
+ end
219
+
220
+
221
+ # indicates if the pid file exists for this service
222
+ #---------------------------------------------------------------------------
223
+ def _pid_file_exists?
224
+ File.exist?(_pid_filename)
225
+ end
226
+
227
+
228
+ # returns the entire contents of the pid file
229
+ #---------------------------------------------------------------------------
230
+ def _pid_file_content
231
+ begin
232
+ f = File.open(_pid_filename, 'r')
233
+ return f.blank? ? f : f.read
234
+ rescue Errno::ENOENT
235
+ return nil
236
+ end
237
+ end
238
+
239
+
240
+ # the pid found in the pidfile (if it exists, nil if it doesn't)
241
+ #---------------------------------------------------------------------------
242
+ def _pid_file_pid
243
+ /^(\d*)$/ =~ _pid_file_content
244
+ return $1.blank? ? nil : $1.to_i
245
+ end
246
+
247
+
248
+ # the current process' pid
249
+ #---------------------------------------------------------------------------
250
+ def _pid
251
+ @@_pid ||= Process.pid
252
+ end
253
+
254
+
255
+ # Checks to see if the process pointed to by the pid file is actually
256
+ # running Note that I couldn't find a good way to check for the status of a
257
+ # different process by its pid, so this checks to see if the proces has a
258
+ # process group id, and if the process doesn't exist, an exception is
259
+ # returned
260
+ #---------------------------------------------------------------------------
261
+ def _pid_file_process_running?
262
+ begin
263
+ Process.getpgid(_pid_file_pid)
264
+ return true
265
+ rescue
266
+ return false
267
+ end
268
+ end
269
+
270
+
271
+ # returns true if the current process is the pid in the pid file. false
272
+ # otherwise
273
+ #---------------------------------------------------------------------------
274
+ def _process_running?
275
+ return _pid == _pid_file_pid
276
+ end
277
+
278
+ end
279
+ end
280
+
281
+ # gives us the handy acts_as_service method we can declare inside any class
282
+ #-------------------------------------------------------------------------------
283
+ class Object
284
+ def self.acts_as_service
285
+ self.send(:include, ActsAsService)
286
+ end
287
+ end
288
+
289
+
290
+ # totally addicted to blank?
291
+ #-------------------------------------------------------------------------------
292
+ class File
293
+ def blank?
294
+ return self.nil?
295
+ end
296
+ end
297
+ class String
298
+ def blank?
299
+ return self.nil? || self == ''
300
+ end
301
+ end
302
+ class NilClass
303
+ def blank?
304
+ return self.nil?
305
+ end
306
+ end
metadata ADDED
@@ -0,0 +1,67 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: acts_as_service
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2
5
+ platform: ruby
6
+ authors:
7
+ - Brian Percival
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2010-02-18 00:00:00 -08:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description: |
17
+ A gem with a mixin to let you turn a class into something that runs like a service,
18
+ which you can MyService.start, MyService.stop, and MyService.restart. It tracks
19
+ its own pid. For now, pretty sure it requires that the class is running inside
20
+ a rails context (e.g. run by script/runner MyService.start), but that could
21
+ probably be changed without too much difficulty.
22
+
23
+ email: percivalatumamibuddotcom
24
+ executables: []
25
+
26
+ extensions: []
27
+
28
+ extra_rdoc_files:
29
+ - LICENSE
30
+ - README.rdoc
31
+ - TODO
32
+ files:
33
+ - README.rdoc
34
+ - acts_as_service.gemspec
35
+ - lib/acts_as_service.rb
36
+ - LICENSE
37
+ - TODO
38
+ has_rdoc: true
39
+ homepage: http://github.com/bmpercy/acts_as_service
40
+ licenses: []
41
+
42
+ post_install_message:
43
+ rdoc_options:
44
+ - --charset=UTF-8
45
+ require_paths:
46
+ - lib
47
+ required_ruby_version: !ruby/object:Gem::Requirement
48
+ requirements:
49
+ - - ">="
50
+ - !ruby/object:Gem::Version
51
+ version: "0"
52
+ version:
53
+ required_rubygems_version: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ version: "0"
58
+ version:
59
+ requirements: []
60
+
61
+ rubyforge_project:
62
+ rubygems_version: 1.3.5
63
+ signing_key:
64
+ specification_version: 3
65
+ summary: Makes it very easy to create a service-like class that's easy to start and stop, taken from work at discovereads.com
66
+ test_files: []
67
+