Class: Ringleader::Process

Inherits:
Object
  • Object
show all
Includes:
Celluloid, Celluloid::Logger, NameLogger
Defined in:
lib/ringleader/process.rb

Overview

Represents an instance of a configured application.

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from NameLogger

#debug, #error, #info, #warn, #with_name

Constructor Details

#initialize(config) ⇒ Process

Create a new App instance.

config - a configuration object for this app



14
15
16
17
# File 'lib/ringleader/process.rb', line 14

def initialize(config)
  @config = config
  @starting = @running = false
end

Instance Attribute Details

#configObject (readonly)

Returns the value of attribute config.



9
10
11
# File 'lib/ringleader/process.rb', line 9

def config
  @config
end

Instance Method Details

#already_running?Boolean

Internal: check if the app is already running outside ringleader

Returns:

  • (Boolean)


159
160
161
162
163
164
165
166
167
# File 'lib/ringleader/process.rb', line 159

def already_running?
  socket = TCPSocket.new config.host, config.app_port
  socket.close
  true
rescue Errno::ECONNREFUSED, Errno::ETIMEDOUT
  false
rescue IOError, SystemCallError => e
  error "unexpected error when checking status: #{e}"
end

#child_pids(parent_pid) ⇒ Object

Internal: returns all child pids of the given parent



204
205
206
207
208
# File 'lib/ringleader/process.rb', line 204

def child_pids(parent_pid)
  debug "retrieving child pids of #{parent_pid}"
  proc_table = Sys::ProcTable.ps
  children_of parent_pid, proc_table
end

#children_of(parent_pid, proc_table) ⇒ Object

Internal: find child pids given a parent pid and a proc table



211
212
213
214
215
216
217
218
219
220
# File 'lib/ringleader/process.rb', line 211

def children_of(parent_pid, proc_table)
  [].tap do |pids|
    proc_table.each do |proc_record|
      if proc_record.ppid == parent_pid
        pids << proc_record.pid
        pids.concat children_of proc_record.pid, proc_table
      end
    end
  end
end

#exitedObject

Internal: callback for when the process has exited.



91
92
93
94
95
96
97
98
# File 'lib/ringleader/process.rb', line 91

def exited
  info "pid #{@pid} exited"
  @running = false
  @pid = nil
  @wait_for_port.terminate if @wait_for_port.alive?
  @wait_for_exit.terminate if @wait_for_exit.alive?
  signal :running, false
end

#in_clean_environment(&block) ⇒ Object

Internal: execute a command in a clean environment (bundler)



181
182
183
184
185
186
187
# File 'lib/ringleader/process.rb', line 181

def in_clean_environment(&block)
  if Object.const_defined?(:Bundler)
    ::Bundler.with_clean_env(&block)
  else
    yield
  end
end

#port_openedObject

Internal: callback for when the application port has opened



85
86
87
88
# File 'lib/ringleader/process.rb', line 85

def port_opened
  info "listening on #{config.host}:#{config.app_port}"
  signal :running, true
end

#proxy_output(input) ⇒ Object

Internal: proxy output streams to the logger.

Fire and forget, runs in its own thread.



172
173
174
175
176
177
178
# File 'lib/ringleader/process.rb', line 172

def proxy_output(input)
  Thread.new do
    until input.eof?
      info input.gets.strip
    end
  end
end

#reap_orphans(pids) ⇒ Object

Internal: kill orphaned processes



190
191
192
193
194
195
196
197
198
199
200
201
# File 'lib/ringleader/process.rb', line 190

def reap_orphans(pids)
  pids.each do |pid|
    debug "checking for child #{pid}"
    next unless Sys::ProcTable.ps(pid)
    error "child process #{pid} was orphaned, killing it"
    begin
      ::Process.kill "KILL", pid
    rescue Errno::ESRCH, Errno::EPERM
      debug "could not kill #{pid}"
    end
  end
end

#running?Boolean

Public: query if the app is running

Returns:

  • (Boolean)


20
21
22
# File 'lib/ringleader/process.rb', line 20

def running?
  @running
end

#startObject

Public: start the application.

This method is intended to be used synchronously. If the app is already running, it’ll return immediately. If the app hasn’t been started, or is in the process of starting, this method blocks until it starts or fails to start correctly.

Returns true if the app started, false if not.



32
33
34
35
36
37
38
39
40
41
42
43
44
45
# File 'lib/ringleader/process.rb', line 32

def start
  if @running
    true
  elsif @starting
    wait :running
  else
    if already_running?
      warn "#{config.name} already running on port #{config.app_port}"
      return true
    else
      start_app
    end
  end
end

#start_appObject

Internal: start the application process and associated infrastructure

Intended to be synchronous, as it blocks until the app has started (or failed to start).

Returns true if the app started, false if not.



106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
# File 'lib/ringleader/process.rb', line 106

def start_app
  @starting = true
  info "starting process `#{config.command}`"

  unless File.directory?(config.dir)
    error "#{config.dir} does not exist!"
    @starting = false
    @running = false
    return false
  end

  # give the child process a terminal so output isn't buffered
  @master, slave = PTY.open
  in_clean_environment do
    @pid = ::Process.spawn(
      config.env,
      %Q(bash -c "#{config.command}"),
      :in => slave,
      :out => slave,
      :err => slave,
      :chdir => config.dir,
      :pgroup => true
    )
  end
  slave.close
  proxy_output @master
  debug "started with pid #{@pid}"

  @wait_for_exit = WaitForExit.new @pid, Actor.current
  @wait_for_port = WaitForPort.new config.host, config.app_port, Actor.current

  timer = after config.startup_timeout do
    warn "application startup took more than #{config.startup_timeout}"
    async.stop
  end

  @running = wait :running

  @starting = false
  timer.cancel

  @running
rescue Errno::ENOENT
  @starting = false
  @running = false
  false
ensure
  unless @running
    warn "could not start `#{config.command}`"
  end
end

#stopObject

Public: stop the application.

Sends a SIGTERM to the app’s process, and expects it to exit like a sane and well-behaved application within 7 seconds before sending a SIGKILL.

Uses config.kill_with for the initial signal, which defaults to “TERM”. If a configured process doesn’t respond well to TERM (i.e. leaving zombies), use KILL instead.



55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
# File 'lib/ringleader/process.rb', line 55

def stop
  return unless @pid

  children = child_pids @pid

  info "stopping #{@pid}"
  debug "child pids: #{children.inspect}"

  @master.close unless @master.closed?

  debug "kill -#{config.kill_with} #{@pid}"
  ::Process.kill config.kill_with, -@pid

  failsafe = after 7 do
    warn "process #{@pid} did not shut down cleanly, killing it"
    debug "kill -KILL #{@pid}"
    ::Process.kill "KILL", -@pid
    reap_orphans children
  end

  wait :running # wait for the exit callback
  failsafe.cancel
  sleep 2 # give the children a chance to shut down
  reap_orphans children

rescue Errno::ESRCH, Errno::EPERM
  exited
end