euston-daemons 1.0.5 → 1.2.1

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.
Files changed (69) hide show
  1. data/Gemfile +4 -1
  2. data/Rakefile +2 -3
  3. data/euston-daemons.gemspec +50 -39
  4. data/lib/euston-daemons.rb +14 -1
  5. data/lib/euston-daemons/euston/daemon.rb +99 -0
  6. data/lib/euston-daemons/euston/daemon_component.rb +25 -0
  7. data/lib/euston-daemons/euston/daemon_component_host.rb +66 -0
  8. data/lib/euston-daemons/euston/daemon_environment.rb +59 -0
  9. data/lib/euston-daemons/euston/exceptions.rb +9 -0
  10. data/lib/euston-daemons/euston/stopwatch.rb +15 -0
  11. data/lib/euston-daemons/pipeline/config/environment.rb +78 -0
  12. data/lib/euston-daemons/pipeline/lib/command_logger/component.rb +54 -0
  13. data/lib/euston-daemons/pipeline/lib/command_logger/log.rb +31 -0
  14. data/lib/euston-daemons/pipeline/lib/command_processor/component.rb +50 -0
  15. data/lib/euston-daemons/pipeline/lib/command_processor/default_commands/retry_failed_message.rb +13 -0
  16. data/lib/euston-daemons/pipeline/lib/command_processor/default_handlers/retry_failed_message.rb +34 -0
  17. data/lib/euston-daemons/pipeline/lib/command_processor/failed_message.rb +36 -0
  18. data/lib/euston-daemons/pipeline/lib/daemon.rb +85 -0
  19. data/lib/euston-daemons/pipeline/lib/event_processor/component.rb +67 -0
  20. data/lib/euston-daemons/pipeline/lib/event_processor/default_handlers/message_failure.rb +30 -0
  21. data/lib/euston-daemons/pipeline/lib/event_store_dispatcher/component.rb +68 -0
  22. data/lib/euston-daemons/pipeline/lib/message_buffer/buffer.rb +85 -0
  23. data/lib/euston-daemons/pipeline/lib/message_buffer/component.rb +59 -0
  24. data/lib/euston-daemons/pipeline/lib/snapshotter/component.rb +48 -0
  25. data/lib/euston-daemons/pipeline/rake_task.rb +49 -0
  26. data/lib/euston-daemons/rake_task.rb +63 -66
  27. data/lib/euston-daemons/rake_tasks.rb +3 -5
  28. data/lib/euston-daemons/version.rb +1 -1
  29. data/spec/daemons/command_processor_spec.rb +48 -0
  30. data/spec/daemons/event_processor_spec.rb +55 -0
  31. data/spec/daemons/message_buffer_spec.rb +106 -0
  32. data/spec/daemons/snapshotter_spec.rb +96 -0
  33. data/spec/spec_helper.rb +91 -0
  34. data/spec/support/factories/commands.rb +16 -0
  35. data/spec/support/factories/commit.rb +7 -0
  36. data/spec/support/factories/event_message.rb +12 -0
  37. data/spec/support/factories/events.rb +8 -0
  38. data/spec/support/filters.rb +13 -0
  39. data/spec/support/sample_model/commands.rb +14 -0
  40. data/spec/support/sample_model/counter.rb +36 -0
  41. data/spec/support/sample_model/counter2.rb +46 -0
  42. data/spec/support/stub_retrying_subscription.rb +9 -0
  43. metadata +131 -67
  44. data/lib/euston-daemons/command_processor_daemon/config/environment.rb +0 -25
  45. data/lib/euston-daemons/command_processor_daemon/lib/components/command_handler_component.rb +0 -56
  46. data/lib/euston-daemons/command_processor_daemon/lib/daemon.rb +0 -43
  47. data/lib/euston-daemons/command_processor_daemon/lib/settings.rb +0 -22
  48. data/lib/euston-daemons/command_processor_daemon/rake_task.rb +0 -34
  49. data/lib/euston-daemons/event_processor_daemon/config/environment.rb +0 -25
  50. data/lib/euston-daemons/event_processor_daemon/lib/components/event_handler_component.rb +0 -58
  51. data/lib/euston-daemons/event_processor_daemon/lib/daemon.rb +0 -71
  52. data/lib/euston-daemons/event_processor_daemon/lib/settings.rb +0 -26
  53. data/lib/euston-daemons/event_processor_daemon/rake_task.rb +0 -37
  54. data/lib/euston-daemons/framework/basic_component.rb +0 -33
  55. data/lib/euston-daemons/framework/channel_thread.rb +0 -22
  56. data/lib/euston-daemons/framework/component_shutdown.rb +0 -22
  57. data/lib/euston-daemons/framework/daemon.rb +0 -27
  58. data/lib/euston-daemons/framework/handler_bindings_component.rb +0 -56
  59. data/lib/euston-daemons/framework/queue.rb +0 -71
  60. data/lib/euston-daemons/message_buffer_daemon/config/environment.rb +0 -28
  61. data/lib/euston-daemons/message_buffer_daemon/lib/components/buffer_component.rb +0 -73
  62. data/lib/euston-daemons/message_buffer_daemon/lib/components/event_store_component.rb +0 -52
  63. data/lib/euston-daemons/message_buffer_daemon/lib/daemon.rb +0 -48
  64. data/lib/euston-daemons/message_buffer_daemon/lib/message_logger.rb +0 -54
  65. data/lib/euston-daemons/message_buffer_daemon/lib/publisher.rb +0 -56
  66. data/lib/euston-daemons/message_buffer_daemon/lib/read_model/message_log.rb +0 -36
  67. data/lib/euston-daemons/message_buffer_daemon/lib/settings.rb +0 -14
  68. data/lib/euston-daemons/message_buffer_daemon/lib/subscriber.rb +0 -60
  69. data/lib/euston-daemons/message_buffer_daemon/rake_task.rb +0 -30
@@ -0,0 +1,30 @@
1
+ module Euston
2
+ module Daemons
3
+ module Pipeline
4
+ module EventProcessor
5
+ module DefaultHandlers
6
+ class MessageFailure
7
+ include Euston::EventHandler
8
+
9
+ subscribes :message_failed, 1 do |headers, event|
10
+ headers = event[:message][:headers].dup
11
+ failure = { :message_id => headers.delete(:id),
12
+ :type => headers.delete(:type),
13
+ :version => headers.delete(:version),
14
+ :message_timestamp => headers.delete(:timestamp),
15
+ :routing_key => event[:routing_key],
16
+ :body => event[:message][:body],
17
+ :headers => headers,
18
+ :error => event[:error],
19
+ :backtrace => event[:backtrace],
20
+ :failure_timestamp => Time.now.to_f }
21
+
22
+ failed_messages = CommandProcessor::FailedMessage.new DaemonEnvironment.event_store_mongodb
23
+ failed_messages.log_failure failure
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,68 @@
1
+ module Euston
2
+ module Daemons
3
+ module Pipeline
4
+ module EventStoreDispatcher
5
+ class Component < DaemonComponent
6
+ extend RabbitMq::Exchanges
7
+
8
+ def initialize channel, id = 1, log = Euston::NullLogger.instance
9
+ @channel = channel
10
+ @channel.tx_select
11
+ @id = id
12
+ @log = log
13
+ @event_store = DaemonEnvironment.event_store
14
+ @stopwatch = Stopwatch.new.when(:finished => method(:log_elapsed_time))
15
+ @commands_exchange = self.class.get_exchange channel, :commands
16
+ @events_exchange = self.class.get_exchange channel, :events
17
+ @buffer = MessageBuffer::Buffer.new DaemonEnvironment.event_store_mongodb
18
+ end
19
+
20
+ private
21
+
22
+ def log_elapsed_time elapsed_time
23
+ @log.debug "Event store dispatcher #{@id} dispatched #{@commits_dispatched} commit(s) in #{elapsed_time} sec(s)" if @commits_dispatched > 0
24
+ end
25
+
26
+ def next_iteration
27
+ @commits_dispatched = 0
28
+
29
+ @stopwatch.time do
30
+ begin
31
+ @event_store.take_ownership_of_undispatched_commits @id
32
+ @commits = @event_store.get_undispatched_commits @id
33
+
34
+ begin
35
+ @commits.each do |commit|
36
+ commit.events.each do |event|
37
+ hash = event.to_hash
38
+ @events_exchange.publish ActiveSupport::JSON.encode(hash), self.class.default_publish_options.merge(:routing_key => "events.#{hash[:headers][:type]}")
39
+ end
40
+
41
+ commit.commands.each do |command|
42
+ hash = command.to_hash
43
+
44
+ if hash[:headers][:dispatch_at].nil?
45
+ @commands_exchange.publish ActiveSupport::JSON.encode(hash), self.class.default_publish_options.merge(:routing_key => "commands.#{hash[:headers][:type]}")
46
+ else
47
+ @buffer.enqueue :commands, hash, hash[:headers][:dispatch_at]
48
+ end
49
+ end
50
+ end
51
+
52
+ @channel.tx_commit
53
+ rescue StandardError => e
54
+ @channel.tx_rollback
55
+ raise e
56
+ end
57
+
58
+ @event_store.mark_commits_as_dispatched @commits
59
+
60
+ @commits_dispatched += @commits.size
61
+ end until stopped || @commits.empty?
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,85 @@
1
+ module Euston
2
+ module Daemons
3
+ module Pipeline
4
+ module MessageBuffer
5
+ class Buffer
6
+ def initialize mongodb
7
+ name = 'message_buffer'
8
+ mongodb.create_collection name unless mongodb.collection_names.include? name
9
+
10
+ @name = name
11
+ @collection = mongodb.collection name
12
+
13
+ @collection.ensure_index [ ['message_id', Mongo::ASCENDING] ],
14
+ :unique => false,
15
+ :name => "#{name}_message_id_index"
16
+
17
+ @collection.ensure_index [ ['component_id', Mongo::ASCENDING],
18
+ ['dispatch_at', Mongo::ASCENDING] ],
19
+ :unique => false,
20
+ :name => "#{name}_component_id_dispatch_at_index"
21
+ end
22
+
23
+ attr_reader :name
24
+
25
+ def delete_dispatched_messages component_id
26
+ @collection.remove({ 'component_id' => component_id }, :multi => true)
27
+ end
28
+
29
+ def enqueue exchange, message, dispatch_at = nil
30
+ messages = message
31
+ messages = [{ :hash => message, :dispatch_at => dispatch_at }] unless messages.is_a? Array
32
+
33
+ messages = messages.map do |m|
34
+ message_is_well_formed = m.is_a?(Hash) && m.has_key?(:hash) && m.has_key?(:dispatch_at)
35
+ m = { :hash => m, :dispatch_at => dispatch_at } unless message_is_well_formed
36
+ map_to_document exchange, m
37
+ end
38
+
39
+ @collection.insert(messages) unless messages.empty?
40
+ end
41
+
42
+ def find_dispatchable_messages component_id
43
+ query = { 'component_id' => component_id }
44
+ fields = ['exchange', 'type', 'json']
45
+ sort = [ 'dispatch_at', Mongo::ASCENDING ]
46
+
47
+ @collection.find query, :fields => fields, :sort => sort
48
+ end
49
+
50
+ def get_by_id id
51
+ @collection.find_one 'message_id' => id
52
+ end
53
+
54
+ def take_ownership_of_dispatchable_messages component_id
55
+ new_messages_eligible_for_dispatch = { 'component_id' => '',
56
+ 'dispatch_at' => { '$lte' => Time.now.to_f } }
57
+
58
+ messages_stuck_in_other_components = { 'component_id' => { '$ne' => '' },
59
+ 'dispatch_at' => { '$lte' => Time.now.to_f - 60 } }
60
+
61
+ query = { '$or' => [ new_messages_eligible_for_dispatch, messages_stuck_in_other_components ] }
62
+ doc = { '$set' => { 'component_id' => component_id } }
63
+
64
+ @collection.update query, doc, :multi => true
65
+ end
66
+
67
+ private
68
+
69
+ def map_to_document exchange, message
70
+ hash = message[:hash]
71
+ dispatch_at = (message[:dispatch_at] || Time.now.to_f).to_f
72
+
73
+ { 'message_id' => hash[:headers][:id],
74
+ 'exchange' => exchange,
75
+ 'type' => hash[:headers][:type],
76
+ 'component_id' => '',
77
+ 'dispatch_at' => dispatch_at,
78
+ 'dispatch_at_for_humans' => Time.at(dispatch_at),
79
+ 'json' => ActiveSupport::JSON.encode(hash) }
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,59 @@
1
+ module Euston
2
+ module Daemons
3
+ module Pipeline
4
+ module MessageBuffer
5
+ class Component < Euston::DaemonComponent
6
+ extend RabbitMq::Exchanges
7
+
8
+ def initialize channel, id = 1, logger = Euston::NullLogger.instance
9
+ @channel = channel
10
+ @channel.tx_select
11
+ @id = "message_buffer #{id}"
12
+ @log = logger
13
+ @buffer = Buffer.new DaemonEnvironment.event_store_mongodb
14
+ @stopwatch = Stopwatch.new.when(:finished => method(:log_elapsed_time))
15
+ end
16
+
17
+ private
18
+
19
+ def dispatch_due_messages
20
+ @dispatched_count = 0
21
+
22
+ @stopwatch.time do
23
+ @buffer.take_ownership_of_dispatchable_messages @id
24
+
25
+ begin
26
+ @buffer.find_dispatchable_messages(@id).each do |message|
27
+ exchange = self.class.get_exchange @channel, message['exchange']
28
+ exchange.publish message['json'], self.class.default_publish_options.merge(:routing_key => "#{exchange.name}.#{message['type']}")
29
+ @dispatched_count += 1
30
+ end
31
+
32
+ @channel.tx_commit
33
+ rescue StandardError => e
34
+ @channel.tx_rollback
35
+ raise e
36
+ end
37
+
38
+ @buffer.delete_dispatched_messages @id
39
+ end
40
+
41
+ @dispatched_count
42
+ end
43
+
44
+ def log_elapsed_time elapsed_time
45
+ @log.debug "Message buffer #{@id} dispatched #{@dispatched_count} message(s) in #{elapsed_time} sec(s)" if @dispatched_count > 0
46
+ end
47
+
48
+ def next_iteration
49
+ @messages_dispatched = 0
50
+
51
+ begin
52
+ @messages_dispatched = dispatch_due_messages
53
+ end until stopped || @messages_dispatched.zero?
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,48 @@
1
+ module Euston
2
+ module Daemons
3
+ module Pipeline
4
+ module Snapshotter
5
+ class Component < Euston::DaemonComponent
6
+ def initialize event_store, threshold, id = 1, logger = Euston::NullLogger.instance
7
+ @event_store = event_store
8
+ @threshold = threshold
9
+ @id = id
10
+ @log = logger
11
+ end
12
+
13
+ private
14
+
15
+ def next_iteration
16
+ begin
17
+ stream_heads = @event_store.get_streams_to_snapshot @threshold
18
+ @log.debug "Found #{stream_heads.length} stream(s) eligible for snapshotting (threshold is #{@threshold})" if stream_heads.any?
19
+
20
+ stream_heads.each do |stream_head|
21
+ pair = @event_store.get_snapshot_stream_pair stream_head.stream_id
22
+
23
+ loader = RabbitMq::ConstantLoader.new
24
+ loader.when(:hit => ->(klass) { take_snapshot klass, pair },
25
+ :miss => ->(type) { Safely.report! "Snapshotter was unable to find a class: #{type}" })
26
+
27
+ loader.load pair.stream.committed_headers[:aggregate_type]
28
+ end
29
+ end until stopped || stream_heads.empty?
30
+ end
31
+
32
+ def take_snapshot klass, pair
33
+ instance = klass.hydrate pair.stream, pair.snapshot
34
+ snapshot = instance.take_snapshot
35
+ snapshot = EventStore::Snapshot.new pair.stream.stream_id,
36
+ pair.stream.stream_revision,
37
+ snapshot[:payload],
38
+ :version => snapshot[:version]
39
+
40
+ @log.debug "Writing snapshot: #{snapshot.inspect}"
41
+
42
+ @event_store.add_snapshot snapshot
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,49 @@
1
+ module Euston
2
+ class PipelineRakeTask < Euston::Daemons::RakeTask
3
+ attr_accessor :amqp_config_path,
4
+ :command_handler_namespaces,
5
+ :daemon_config_path,
6
+ :event_handler_namespaces,
7
+ :mongo_config_path,
8
+ :user_defined_components
9
+
10
+ def initialize environment
11
+ super(environment, :pipeline_daemon)
12
+ end
13
+
14
+ def before_creating_task
15
+ @daemon_path = File.expand_path(File.dirname __FILE__) + File::SEPARATOR
16
+ @daemon_class = 'Euston::Daemons::Pipeline::Daemon'
17
+ end
18
+
19
+ def load_daemon_config_file
20
+ @data[:daemon_config] = ErbYaml.read @daemon_config_path, @data[:environment]
21
+ end
22
+
23
+ def initialize_settings
24
+ @logger.debug "AMQP config path: #{@amqp_config_path}"
25
+ @data[:amqp_config_path] = @amqp_config_path
26
+
27
+ @logger.debug "Command handler namespaces: #{@command_handler_namespaces}"
28
+ @data[:command_handler_namespaces] = @command_handler_namespaces
29
+
30
+ @logger.debug "Daemon config path: #{@daemon_config_path}"
31
+
32
+ @logger.debug "Event handler namespaces: #{@event_handler_namespaces}"
33
+ @data[:event_handler_namespaces] = @event_handler_namespaces
34
+
35
+ @logger.debug "Mongo config path: #{@mongo_config_path}"
36
+ @data[:mongo_config_path] = @mongo_config_path
37
+
38
+ @logger.debug "User-defined components: #{@user_defined_components.count}"
39
+ @data[:user_defined_components] = @user_defined_components.to_a.flatten
40
+ end
41
+
42
+ def load_environment
43
+ @logger.debug "Loading environment"
44
+ require 'euston-daemons/pipeline/config/environment'
45
+
46
+ Euston::Daemons::Pipeline::DaemonEnvironment.new(@data).setup
47
+ end
48
+ end
49
+ end
@@ -15,11 +15,8 @@ module Euston
15
15
  # Daemon class. Must be supplied as a string.
16
16
  attr_accessor :daemon_class
17
17
 
18
- # Log file path
19
- #
20
- # default:
21
- # /var/log/euston.log
22
- attr_accessor :log_path
18
+ # Callable. Receives the daemon environment object to allow the user to perform other related config operations.
19
+ attr_accessor :post_setup_callback
23
20
 
24
21
  # Use verbose output. If this is set to true, the task will print the
25
22
  # executed command to stdout.
@@ -28,65 +25,51 @@ module Euston
28
25
  # true
29
26
  attr_accessor :verbose
30
27
 
31
- def initialize(*args)
32
- @name = args.shift || :eustondaemon
33
- @log_path = '/var/log/euston.log'
28
+ def initialize environment, name = :euston_daemon
29
+ @name = name
34
30
  @verbose = true
35
-
36
- Object.const_set :EUSTON_ENV, get_environment unless Object.const_defined? :EUSTON_ENV
37
-
38
- @pid_path = pid_path_for_env + "#{@name}.pid"
31
+ @environment = validate_environment environment
32
+ @data = { :environment => @environment }
39
33
 
40
34
  yield self if block_given?
41
35
  send :before_creating_task if respond_to? :before_creating_task
42
36
 
43
37
  desc("Run a Euston daemon") unless ::Rake.application.last_comment
44
-
45
38
  task name do
46
39
  RakeFileUtils.send(:verbose, verbose) do
40
+ load_daemon_config_file
47
41
  initialize_logger
42
+ initialize_settings
43
+ write_pid_file
48
44
  log_startup
49
- initialize_paths
50
- load_framework
51
- load_environment
45
+ env = load_environment
46
+ post_setup_callback.call env unless post_setup_callback.nil?
52
47
  launch_and_wait_for_exit
53
48
  log_shutdown
49
+ remove_pid_file
54
50
  end
55
51
  end
56
52
  end
57
53
 
58
54
  private
59
55
 
60
- def get_environment
61
- environment = ENV['EUSTON_ENV'].to_s.downcase.to_sym
62
- environments = [:development, :test, :staging, :production]
63
- environment = :development unless environments.include? environment
64
- environment
65
- end
56
+ def initialize_logger
57
+ config = @data[:daemon_config]
58
+ log_path = config[:log_path]
66
59
 
67
- def pid_path_for_env
68
- case get_environment
69
- when :development, :test
70
- '~/var/run/euston/'
71
- else
72
- '/var/run/euston/'
73
- end
74
- end
60
+ raise "Required log path does not exist: #{log_path}" unless Dir.exist? log_path
75
61
 
76
- def initialize_logger
77
- Object.const_set :EUSTON_LOG, Logger.new(@log_path.gsub(/\.log$/, ".#{EUSTON_ENV}.log"))
78
- end
62
+ @data[:logger] = @logger = Logger.new(File.join log_path, "#{@name}.#{@environment}.log")
79
63
 
80
- def initialize_paths
81
- # virtual
64
+ begin
65
+ @logger.level = Logger.const_get config[:log_level].upcase.to_sym
66
+ rescue
67
+ @logger.level = Logger::DEBUG
68
+ end
82
69
  end
83
70
 
84
- def rake_pid
85
- if defined? Java
86
- Java::JavaIo::File.new("/proc/self").canonical_file.name
87
- else
88
- File.readlink("/proc/self")
89
- end
71
+ def initialize_settings
72
+ # virtual
90
73
  end
91
74
 
92
75
  def launch_and_wait_for_exit
@@ -97,26 +80,15 @@ module Euston
97
80
  ns = ns.const_get c.to_sym
98
81
  end
99
82
 
100
- daemon = ns.new
101
- create_pid_file
102
- trap_exit_signals daemon
103
- daemon.run
104
- remove_pid_file
83
+ ns.new(@data).run
105
84
  end
106
85
 
107
- def load_framework
108
- EUSTON_LOG.debug "Loading framework"
109
- require_rel 'framework'
110
- end
111
-
112
- def create_pid_file
113
- File.open( @pid_path, 'w' ) { |f|
114
- f.puts rake_pid
115
- }
86
+ def load_daemon_config_file
87
+ # virtual
116
88
  end
117
89
 
118
- def remove_pid_file
119
- File.delete( @pid_path ) rescue Errno::ENOENT
90
+ def load_environment
91
+ # virtual
120
92
  end
121
93
 
122
94
  def log_shutdown
@@ -130,17 +102,42 @@ module Euston
130
102
  def print_log_banner banner
131
103
  border = '-' * banner.length
132
104
 
133
- EUSTON_LOG.info ''
134
- EUSTON_LOG.info border
135
- EUSTON_LOG.info banner
136
- EUSTON_LOG.info border
137
- EUSTON_LOG.info ''
105
+ @logger.info ''
106
+ @logger.info border
107
+ @logger.info banner
108
+ @logger.info border
109
+ @logger.info ''
138
110
  end
139
111
 
140
- def trap_exit_signals daemon
141
- signals = %W(INT TERM) & Signal.list.keys
142
- signals.each { |sig| sig.freeze }.freeze
143
- signals.each { |sig| Signal.trap(sig) { daemon.queue.push(sig) } }
112
+ def remove_pid_file
113
+ @logger.debug "Deleting pid file at #{@pid_file}"
114
+
115
+ File.delete @pid_file rescue Errno::ENOENT
116
+ end
117
+
118
+ def validate_environment environment
119
+ environment = environment.to_s.downcase.to_sym
120
+ environments = [:development, :test, :staging, :production]
121
+ environment = :development unless environments.include? environment
122
+ environment
123
+ end
124
+
125
+ def write_pid_file
126
+ pid_path = @data[:daemon_config][:pid_path]
127
+
128
+ raise "Required pid path does not exist: #{pid_path}" unless Dir.exist? pid_path
129
+
130
+ @pid_file = File.join pid_path, "#{@name}.#{@environment}.pid"
131
+
132
+ if defined? Java
133
+ @pid = java.lang.management.ManagementFactory.getRuntimeMXBean().getName().split('@').first
134
+ else
135
+ @pid = File.readlink("/proc/self")
136
+ end
137
+
138
+ @logger.error "Writing pid #{@pid} to #{@pid_file}"
139
+
140
+ File.open(@pid_file, 'w') { |f| f.puts @pid }
144
141
  end
145
142
  end
146
143
  end