Class: Mongrel2::Handler

Inherits:
Object
  • Object
show all
Extended by:
Loggability
Includes:
CZTop::Reactor::SignalHandling, Constants
Defined in:
lib/mongrel2/handler.rb

Overview

Mongrel2 Handler application class. Instances of this class are the applications which connection to one or more Mongrel2 routes and respond to requests.

Example

A dumb, dead-simple example that just returns a plaintext ‘Hello’ document with a timestamp.

# -*- ruby -*-

require 'mongrel2/handler'

class HelloWorldHandler < Mongrel2::Handler

  ### The main method to override -- accepts requests and
  ### returns responses.
  def handle( request )
      response = request.response

      response.status = 200
      response.headers.content_type = 'text/plain'
      response.puts "Hello, world, it's #{Time.now}!"

      return response
  end

end # class HelloWorldHandler

HelloWorldHandler.run( 'helloworld-handler' )

This assumes the Mongrel2 SQLite config database is in the current directory, and is named ‘config.sqlite’ (the Mongrel2 default), but if it’s somewhere else, you can point the Mongrel2::Config class to it:

require 'mongrel2/config'
Mongrel2::Config.configure( :configdb => 'mongrel2.db' )

Mongrel2 also includes support for Configurability, so you can configure it along with your database connection, etc. Just add a ‘mongrel2’ section to the config with a ‘configdb’ key that points to where the Mongrel2 SQLite config database lives:

# config.yaml
db:
  uri: postgres://www@localhost/db01

mongrel2:
  configdb: mongrel2.db

whatever_else:
  ...

Now just loading and installing the config configures Mongrel2 as well:

require 'configurability/config'

config = Configurability::Config.load( 'config.yml' )
config.install

If the Mongrel2 config database isn’t accessible, or you need to configure the Handler’s two 0mq connections yourself for some reason, you can do that, too:

app = HelloWorldHandler.new( 'helloworld-handler',
    'tcp://otherhost:9999', 'tcp://otherhost:9998' )
app.run

Constant Summary collapse

QUEUE_SIGS =

Signals we handle

[
  :INT, :TERM, :HUP, :USR1,
  # :TODO: :QUIT, :WINCH, :USR2, :TTIN, :TTOU
] & Signal.list.keys.map( &:to_sym )

Constants included from Constants

Constants::DATA_DIR, Constants::DEFAULT_CONFIG_SCRIPT, Constants::DEFAULT_CONFIG_URI, Constants::DEFAULT_CONTROL_SOCKET, Constants::MAX_BROADCAST_IDENTS

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(app_id, send_spec, recv_spec) ⇒ Handler

Create a new instance of the handler with the specified app_id, send_spec, and recv_spec.



141
142
143
144
145
146
147
148
# File 'lib/mongrel2/handler.rb', line 141

def initialize( app_id, send_spec, recv_spec ) # :notnew:
  super() # To the signal handler mixin

  @app_id  = app_id

  @conn    = Mongrel2::Connection.new( app_id, send_spec, recv_spec )
  @reactor = nil
end

Instance Attribute Details

#app_idObject (readonly)

The app ID the app was created with



161
162
163
# File 'lib/mongrel2/handler.rb', line 161

def app_id
  @app_id
end

#connObject (readonly)

The handler’s Mongrel2::Connection object.



157
158
159
# File 'lib/mongrel2/handler.rb', line 157

def conn
  @conn
end

#reactorObject

The CZTop::Reactor that manages IO



165
166
167
# File 'lib/mongrel2/handler.rb', line 165

def reactor
  @reactor
end

Class Method Details

.app_instance_for(appid) ⇒ Object

Return an instance of the handler configured for the handler in the currently-loaded Mongrel2 config that corresponds to appid.



116
117
118
119
120
# File 'lib/mongrel2/handler.rb', line 116

def self::app_instance_for( appid )
  send_spec, recv_spec = self.connection_info_for( appid )
  self.log.info "  config specs: %s <-> %s" % [ send_spec, recv_spec ]
  return new( appid, send_spec, recv_spec )
end

.connection_info_for(appid) ⇒ Object

Return the send_spec and recv_spec for the given appid from the current configuration database. Returns nil if no Handler is configured with appid as its sender_id.



125
126
127
128
129
130
131
132
# File 'lib/mongrel2/handler.rb', line 125

def self::connection_info_for( appid )
  self.log.debug "Looking up handler spec for appid %p" % [ appid ]
  hconfig = Mongrel2::Config::Handler.by_send_ident( appid ).first or
    raise ArgumentError, "no handler with a send_ident of %p configured" % [ appid ]

  self.log.debug "  found: %s" % [ hconfig.values ]
  return hconfig.send_spec, hconfig.recv_spec
end

.run(appid) ⇒ Object

Create an instance of the handler using the config from the database with the given appid and run it.



107
108
109
110
111
# File 'lib/mongrel2/handler.rb', line 107

def self::run( appid )
  app = self.app_instance_for( appid )
  self.log.info "Running application %p: %p" % [ appid, app ]
  app.run
end

Instance Method Details

#accept_request(req) ⇒ Object

Read a request from the connection and dispatch it.



271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
# File 'lib/mongrel2/handler.rb', line 271

def accept_request( req )
  self.log.info( req.inspect )
  res = self.dispatch_request( req )

  if res
    self.log.info( res.inspect )
    @conn.reply( res ) unless @conn.closed?
  end
ensure
  # Remove any temporarily spooled Mongrel2 files.
  begin
    if req && req.body && req.body.respond_to?( :path ) && req.body.path
      req.body.close unless req.body.closed?
      File.unlink( req.body.path )
    end
  rescue Errno::ENOENT => err
    self.log.debug "File already cleaned up: %s (%s)" % [ req.body.path, err.message ]
  end
end

#configured_handlersObject

Return the Mongrel2::Config::Handlers that corresponds to this app’s appid.



197
198
199
# File 'lib/mongrel2/handler.rb', line 197

def configured_handlers
  return Mongrel2::Config::Handler.by_send_ident( self.app_id )
end

#configured_hostsObject

Return the Mongrel2::Config::Hosts that have routes that point to this Handler.



211
212
213
214
# File 'lib/mongrel2/handler.rb', line 211

def configured_hosts
  routes = self.configured_routes
  return Mongrel2::Config::Host.where( id: routes.select(:host_id) )
end

#configured_routesObject

Return the Mongre2::Config::Routes for this Handler.



203
204
205
206
# File 'lib/mongrel2/handler.rb', line 203

def configured_routes
  handlers = self.configured_handlers
  return Mongrel2::Config::Route.where( target_id: handlers.select(:id) )
end

#configured_serversObject

Return the Mongrel2::Config::Servers that have hosts that have routes that point to this Handler.



219
220
221
222
# File 'lib/mongrel2/handler.rb', line 219

def configured_servers
  hosts = self.configured_hosts
  return Mongrel2::Config::Server.where( id: hosts.select(:server_id) )
end

#dispatch_request(request) ⇒ Object

Invoke a handler method appropriate for the given request.



293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
# File 'lib/mongrel2/handler.rb', line 293

def dispatch_request( request )
  if request.is_disconnect?
    self.log.debug "disconnect!"
    self.handle_disconnect( request )
    return nil

  elsif request.upload_started?
    self.log.debug "async upload start!"
    return self.handle_async_upload_start( request )

  else
    self.log.debug "%s request." % [ request.headers['METHOD'] ]
    case request
    when Mongrel2::WebSocket::ClientHandshake
      return self.handle_websocket_handshake( request )
    when Mongrel2::WebSocket::Request
      return self.handle_websocket( request )
    when Mongrel2::HTTPRequest
      return self.handle( request )
    when Mongrel2::JSONRequest
      return self.handle_json( request )
    when Mongrel2::XMLRequest
      return self.handle_xml( request )
    else
      self.log.error "Unhandled request type %s (%p)" %
        [ request.headers['METHOD'], request.class ]
      return nil
    end
  end
end

#handle(request) ⇒ Object

The main handler function: handle the specified HTTP request (a Mongrel2::Request) and return a response (Mongrel2::Response). If not overridden, this method returns a ‘204 No Content’ response.



347
348
349
350
351
352
353
# File 'lib/mongrel2/handler.rb', line 347

def handle( request )
  self.log.warn "No default handler; responding with '204 No Content'"
  response = request.response
  response.status = HTTP::NO_CONTENT

  return response
end

#handle_async_upload_start(request) ⇒ Object

Handle an asynchronous upload start notification. These are sent to notify the handler that a request that exceeds the server’s limits.content_length has been received. The default implementation cancels any such uploads by replying with an empty string. If the request should be accepted, your handler should override this and do nothing if the request should continue. You’ll receive a new request via the regular callback when the upload completes whose entity body is open to the spooled file.



411
412
413
414
415
416
417
418
419
420
421
422
423
424
# File 'lib/mongrel2/handler.rb', line 411

def handle_async_upload_start( request )
  explanation = "  If you wish to handle requests like this, either set your server's\n  'limits.content_length' setting to a higher value than %d, or override\n  #handle_async_upload_start.\n  END_MESSAGE\n\n  self.log.warn \"Async upload from %s dropped.\" % [ request.remote_ip ]\n  self.log.info( explanation )\n\n  self.conn.reply_close( request )\n\n  return nil\nend\n" % [ request.content_length ]

#handle_disconnect(request) ⇒ Object

Handle a disconnect notice from Mongrel2 via the given request. Its return value is ignored.



398
399
400
401
# File 'lib/mongrel2/handler.rb', line 398

def handle_disconnect( request )
  self.log.info "Connection %p closed." % [ request.conn_id ]
  return nil
end

#handle_json(request) ⇒ Object

Handle a JSON message request. If not overridden, JSON message (‘@route’) requests are ignored.



358
359
360
361
# File 'lib/mongrel2/handler.rb', line 358

def handle_json( request )
  self.log.warn "Unhandled JSON message request (%p)" % [ request.headers.path ]
  return nil
end

#handle_websocket(request) ⇒ Object

Handle a WebSocket frame in request. If not overridden, WebSocket connections are closed with a policy error status.



374
375
376
377
378
379
380
381
382
383
# File 'lib/mongrel2/handler.rb', line 374

def handle_websocket( request )
  self.log.warn "Unhandled WEBSOCKET frame (%p)" % [ request.headers.path ]
  res = request.response
  res.make_close_frame( Mongrel2::WebSocket::CLOSE_POLICY_VIOLATION )

  self.conn.reply( res )
  self.conn.reply_close( request )

  return nil
end

#handle_websocket_handshake(handshake) ⇒ Object

Handle a WebSocket handshake HTTP request. If not overridden, this method drops the connection.



388
389
390
391
392
393
# File 'lib/mongrel2/handler.rb', line 388

def handle_websocket_handshake( handshake )
  self.log.warn "Unhandled WEBSOCKET_HANDSHAKE request (%p)" % [ handshake.headers.path ]
  self.conn.reply_close( handshake )

  return nil
end

#handle_xml(request) ⇒ Object

Handle an XML message request. If not overridden, XML message (‘<route’) requests are ignored.



366
367
368
369
# File 'lib/mongrel2/handler.rb', line 366

def handle_xml( request )
  self.log.warn "Unhandled XML message request (%p)" % [ request.headers.pack ]
  return nil
end

#handler_configObject

Return the Mongrel2::Config::Handler that corresponds to this app’s appid, and its connection’s send_spec and recv_spec.



187
188
189
190
191
192
# File 'lib/mongrel2/handler.rb', line 187

def handler_config
  return self.configured_handlers.where(
    send_spec: self.conn.sub_addr,
    recv_spec: self.conn.pub_addr
  ).first
end

#inspectObject

Returns a string containing a human-readable representation of the Handler suitable for debugging.



327
328
329
330
331
332
333
# File 'lib/mongrel2/handler.rb', line 327

def inspect
  return "#<%p:0x%016x conn: %p>" % [
    self.class,
    self.object_id * 2,
    self.conn,
  ]
end

#on_socket_event(event) ⇒ Object

Reactor callback – handle an IO event.



258
259
260
261
262
263
264
265
266
267
# File 'lib/mongrel2/handler.rb', line 258

def on_socket_event( event )
  if event.readable?
    req = self.conn.receive
    self.accept_request( req )
  elsif event.writable?
    raise "Request socket became writable?!"
  else
    raise "Socket event was neither readable nor writable! (%s)" % [ event ]
  end
end

#restartObject

Restart the handler. You should override this if you want to re-establish database connections, flush caches, or other restart-ey stuff.



235
236
237
238
239
240
241
242
243
244
245
246
247
# File 'lib/mongrel2/handler.rb', line 235

def restart
  raise "can't restart: not running" unless self.reactor

  self.log.info "Restarting"
  if (( old_conn = @conn ))
    self.reactor.unregister( old_conn.request_sock )
    @conn = @conn.dup
    self.reactor.register( @conn.request_sock, :read, &self.method(:on_socket_event) )

    self.log.debug "  conn %p -> %p" % [ old_conn, @conn ]
    old_conn.close
  end
end

#runObject

Run the handler.



169
170
171
172
173
174
175
176
177
178
179
180
181
182
# File 'lib/mongrel2/handler.rb', line 169

def run
  self.log.info "Starting up %p" % [ self ]

  self.reactor = CZTop::Reactor.new
  self.reactor.register( @conn.request_sock, :read, &self.method(:on_socket_event) )
  self.with_signal_handler( self.reactor, *QUEUE_SIGS ) do
    self.start_accepting_requests
  end

  return self # For chaining
ensure
  self.log.info "Done: %p" % [ self ]
  @conn.close if @conn
end

#shutdownObject

Shut down the handler.



226
227
228
229
230
# File 'lib/mongrel2/handler.rb', line 226

def shutdown
  self.log.info "Shutting down."
  self.reactor.stop_polling
  @conn.close
end

#start_accepting_requestsObject

Start a loop, accepting a request and handling it.



251
252
253
254
# File 'lib/mongrel2/handler.rb', line 251

def start_accepting_requests
  self.log.info "Starting the request loop."
  self.reactor.start_polling( ignore_interrupts: true )
end