Commit f1f9d02d authored by Chris McDonough's avatar Chris McDonough

These patches provide clean signal handling and logfile rotation to Zope.

All Zope process will respond to signals in the specified manner:

  SIGHUP -  close open database connections and sockets, then restart the
            process

  SIGTERM - close open database connections and sockets, then shut down.

  SIGINT  - same as SIGTERM

  SIGUSR2 - rotate all Zope log files (z2.log, event log, detailed log)

The common idiom for doing automated logfile rotation will become:

kill -USR2 `cat /path/to/var/z2.pid`

The common idiom for doing "prophylactic" restarts will become:

kill -HUP `cat /path/to/var/z2.pid`

When a process is interrupted via ctrl-C or via a TERM signal (INT, TERM),
all open database connections and sockets will be closed before
the process dies.  This will speed up restart time for sites that
use a FileStorage as its index will be written to the filesystem before
shutdown.

Unspecified signals kill the process without doing cleanup.
parent b087d5f4
...@@ -39,12 +39,18 @@ class DebugLogger: ...@@ -39,12 +39,18 @@ class DebugLogger:
""" """
def __init__(self, filename): def __init__(self, filename):
self.filename = filename
self.file=open(filename, 'a+b') self.file=open(filename, 'a+b')
l=thread.allocate_lock() l=thread.allocate_lock()
self._acquire=l.acquire self._acquire=l.acquire
self._release=l.release self._release=l.release
self.log('U', '000000000', 'System startup') self.log('U', '000000000', 'System startup')
def reopen(self):
self.file.close()
self.file=open(self.filename, 'a+b')
self.log('U', '000000000', 'Logfile reopened')
def log(self, code, request_id, data=''): def log(self, code, request_id, data=''):
self._acquire() self._acquire()
try: try:
......
...@@ -15,5 +15,24 @@ ...@@ -15,5 +15,24 @@
""" """
from ZServer.medusa.thread.select_trigger import trigger from ZServer.medusa.thread.select_trigger import trigger
from ZServer.medusa.asyncore import socket_map
class simple_trigger(trigger):
def handle_close(self):
pass
the_trigger=simple_trigger()
def Wakeup(thunk=None):
try:
the_trigger.pull_trigger(thunk)
except OSError, why:
# this broken pipe as a result of perhaps a signal
# we want to handle this gracefully so we get rid of the old
# trigger and install a new one.
if why[0] == 32:
del socket_map[the_trigger._fileno]
global the_trigger
the_trigger = simple_trigger() # adds itself back into socket_map
the_trigger.pull_trigger(thunk)
Wakeup=trigger().pull_trigger
...@@ -353,6 +353,10 @@ class dispatcher: ...@@ -353,6 +353,10 @@ class dispatcher:
if why[0] in [ECONNRESET, ENOTCONN, ESHUTDOWN]: if why[0] in [ECONNRESET, ENOTCONN, ESHUTDOWN]:
self.handle_close() self.handle_close()
return '' return ''
if why[0] == EAGAIN:
# Happens as a result of a nonfatal signal when select is
# interrupted
return ''
else: else:
raise socket.error, why raise socket.error, why
...@@ -524,12 +528,15 @@ if os.name == 'posix': ...@@ -524,12 +528,15 @@ if os.name == 'posix':
# NOTE: this is a difference from the Python 2.2 library # NOTE: this is a difference from the Python 2.2 library
# version of asyncore.py. This prevents a hanging condition # version of asyncore.py. This prevents a hanging condition
# on Linux 2.2 based systems. # on Linux 2.2 based systems.
while 1: i = 0
while i < 5: # this is a guess
try: try:
return apply (os.read, (self.fd,)+args) return apply (os.read, (self.fd,)+args)
except exceptions.OSError, why: except exceptions.OSError, why:
if why[0] != EAGAIN: if why[0] != EAGAIN:
raise raise
else:
i = i + 1
def send (self, *args): def send (self, *args):
return apply (os.write, (self.fd,)+args) return apply (os.write, (self.fd,)+args)
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
# All Rights Reserved. # All Rights Reserved.
# #
RCS_ID = '$Id: http_server.py,v 1.31 2002/03/21 15:48:53 htrd Exp $' RCS_ID = '$Id: http_server.py,v 1.32 2002/06/11 22:02:47 chrism Exp $'
# python modules # python modules
import os import os
...@@ -627,20 +627,26 @@ class http_server (asyncore.dispatcher): ...@@ -627,20 +627,26 @@ class http_server (asyncore.dispatcher):
def handle_accept (self): def handle_accept (self):
self.total_clients.increment() self.total_clients.increment()
try: try:
conn, addr = self.accept() tup = self.accept()
except socket.error: except socket.error:
# linux: on rare occasions we get a bogus socket back from # linux: on rare occasions we get a bogus socket back from
# accept. socketmodule.c:makesockaddr complains that the # accept. socketmodule.c:makesockaddr complains that the
# address family is unknown. We don't want the whole server # address family is unknown. We don't want the whole server
# to shut down because of this. # to shut down because of this.
self.log_info ('warning: server accept() threw an exception', 'warning') self.log_info ('warning: server accept() threw an exception',
'warning')
self.total_clients.decrement()
return return
try:
conn, addr = tup
except TypeError: except TypeError:
# unpack non-sequence. this can happen when a read event # unpack non-sequence. this can happen when a read event
# fires on a listening socket, but when we call accept() # fires on a listening socket, but when we call accept()
# we get EWOULDBLOCK, so dispatcher.accept() returns None. # we get EWOULDBLOCK, so dispatcher.accept() returns None.
# Seen on FreeBSD3. # Seen on FreeBSD3 and Linux.
self.log_info ('warning: server accept() threw EWOULDBLOCK', 'warning') #self.log_info ('warning: server accept() returned %s '
# '(EWOULDBLOCK?)' % tup, 'warning')
self.total_clients.decrement()
return return
self.channel_class (self, conn, addr) self.channel_class (self, conn, addr)
......
...@@ -35,16 +35,25 @@ class file_logger: ...@@ -35,16 +35,25 @@ class file_logger:
# pass this either a path or a file object. # pass this either a path or a file object.
def __init__ (self, file, flush=1, mode='a'): def __init__ (self, file, flush=1, mode='a'):
self.filename = None
if type(file) == type(''): if type(file) == type(''):
if (file == '-'): if (file == '-'):
import sys import sys
self.file = sys.stdout self.file = sys.stdout
else: else:
self.filename = file
self.file = open (file, mode) self.file = open (file, mode)
else: else:
self.file = file self.file = file
self.do_flush = flush self.do_flush = flush
def reopen(self):
if self.filename:
self.file.close()
self.file = open(self.filename,'a')
def __repr__ (self): def __repr__ (self):
return '<file logger: %s>' % self.file return '<file logger: %s>' % self.file
......
...@@ -39,12 +39,18 @@ class DebugLogger: ...@@ -39,12 +39,18 @@ class DebugLogger:
""" """
def __init__(self, filename): def __init__(self, filename):
self.filename = filename
self.file=open(filename, 'a+b') self.file=open(filename, 'a+b')
l=thread.allocate_lock() l=thread.allocate_lock()
self._acquire=l.acquire self._acquire=l.acquire
self._release=l.release self._release=l.release
self.log('U', '000000000', 'System startup') self.log('U', '000000000', 'System startup')
def reopen(self):
self.file.close()
self.file=open(self.filename, 'a+b')
self.log('U', '000000000', 'Logfile reopened')
def log(self, code, request_id, data=''): def log(self, code, request_id, data=''):
self._acquire() self._acquire()
try: try:
......
...@@ -15,5 +15,24 @@ ...@@ -15,5 +15,24 @@
""" """
from ZServer.medusa.thread.select_trigger import trigger from ZServer.medusa.thread.select_trigger import trigger
from ZServer.medusa.asyncore import socket_map
class simple_trigger(trigger):
def handle_close(self):
pass
the_trigger=simple_trigger()
def Wakeup(thunk=None):
try:
the_trigger.pull_trigger(thunk)
except OSError, why:
# this broken pipe as a result of perhaps a signal
# we want to handle this gracefully so we get rid of the old
# trigger and install a new one.
if why[0] == 32:
del socket_map[the_trigger._fileno]
global the_trigger
the_trigger = simple_trigger() # adds itself back into socket_map
the_trigger.pull_trigger(thunk)
Wakeup=trigger().pull_trigger
...@@ -353,6 +353,10 @@ class dispatcher: ...@@ -353,6 +353,10 @@ class dispatcher:
if why[0] in [ECONNRESET, ENOTCONN, ESHUTDOWN]: if why[0] in [ECONNRESET, ENOTCONN, ESHUTDOWN]:
self.handle_close() self.handle_close()
return '' return ''
if why[0] == EAGAIN:
# Happens as a result of a nonfatal signal when select is
# interrupted
return ''
else: else:
raise socket.error, why raise socket.error, why
...@@ -524,12 +528,15 @@ if os.name == 'posix': ...@@ -524,12 +528,15 @@ if os.name == 'posix':
# NOTE: this is a difference from the Python 2.2 library # NOTE: this is a difference from the Python 2.2 library
# version of asyncore.py. This prevents a hanging condition # version of asyncore.py. This prevents a hanging condition
# on Linux 2.2 based systems. # on Linux 2.2 based systems.
while 1: i = 0
while i < 5: # this is a guess
try: try:
return apply (os.read, (self.fd,)+args) return apply (os.read, (self.fd,)+args)
except exceptions.OSError, why: except exceptions.OSError, why:
if why[0] != EAGAIN: if why[0] != EAGAIN:
raise raise
else:
i = i + 1
def send (self, *args): def send (self, *args):
return apply (os.write, (self.fd,)+args) return apply (os.write, (self.fd,)+args)
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
# All Rights Reserved. # All Rights Reserved.
# #
RCS_ID = '$Id: http_server.py,v 1.31 2002/03/21 15:48:53 htrd Exp $' RCS_ID = '$Id: http_server.py,v 1.32 2002/06/11 22:02:47 chrism Exp $'
# python modules # python modules
import os import os
...@@ -627,20 +627,26 @@ class http_server (asyncore.dispatcher): ...@@ -627,20 +627,26 @@ class http_server (asyncore.dispatcher):
def handle_accept (self): def handle_accept (self):
self.total_clients.increment() self.total_clients.increment()
try: try:
conn, addr = self.accept() tup = self.accept()
except socket.error: except socket.error:
# linux: on rare occasions we get a bogus socket back from # linux: on rare occasions we get a bogus socket back from
# accept. socketmodule.c:makesockaddr complains that the # accept. socketmodule.c:makesockaddr complains that the
# address family is unknown. We don't want the whole server # address family is unknown. We don't want the whole server
# to shut down because of this. # to shut down because of this.
self.log_info ('warning: server accept() threw an exception', 'warning') self.log_info ('warning: server accept() threw an exception',
'warning')
self.total_clients.decrement()
return return
try:
conn, addr = tup
except TypeError: except TypeError:
# unpack non-sequence. this can happen when a read event # unpack non-sequence. this can happen when a read event
# fires on a listening socket, but when we call accept() # fires on a listening socket, but when we call accept()
# we get EWOULDBLOCK, so dispatcher.accept() returns None. # we get EWOULDBLOCK, so dispatcher.accept() returns None.
# Seen on FreeBSD3. # Seen on FreeBSD3 and Linux.
self.log_info ('warning: server accept() threw EWOULDBLOCK', 'warning') #self.log_info ('warning: server accept() returned %s '
# '(EWOULDBLOCK?)' % tup, 'warning')
self.total_clients.decrement()
return return
self.channel_class (self, conn, addr) self.channel_class (self, conn, addr)
......
...@@ -35,16 +35,25 @@ class file_logger: ...@@ -35,16 +35,25 @@ class file_logger:
# pass this either a path or a file object. # pass this either a path or a file object.
def __init__ (self, file, flush=1, mode='a'): def __init__ (self, file, flush=1, mode='a'):
self.filename = None
if type(file) == type(''): if type(file) == type(''):
if (file == '-'): if (file == '-'):
import sys import sys
self.file = sys.stdout self.file = sys.stdout
else: else:
self.filename = file
self.file = open (file, mode) self.file = open (file, mode)
else: else:
self.file = file self.file = file
self.do_flush = flush self.do_flush = flush
def reopen(self):
if self.filename:
self.file.close()
self.file = open(self.filename,'a')
def __repr__ (self): def __repr__ (self):
return '<file logger: %s>' % self.file return '<file logger: %s>' % self.file
......
...@@ -253,6 +253,75 @@ program=sys.argv[0] ...@@ -253,6 +253,75 @@ program=sys.argv[0]
here=os.path.join(os.getcwd(), os.path.split(program)[0]) here=os.path.join(os.getcwd(), os.path.split(program)[0])
Zpid='' Zpid=''
# Install signal handlers.
# SIGTERM - cleanly shuts down.
# SIGHUP - cleanly restarts.
# SIGUSR2 - open and close all log files (for log rotation)
if os.name == 'posix': # signal.signal() not reliable on Windows
import signal
def closeall():
zLOG.LOG('Z2', zLOG.INFO, "Closing all open network connections")
for socket in asyncore.socket_map.values():
try:
socket.close()
except:
pass
zLOG.LOG('Z2', zLOG.INFO, "Closing all open ZODB databases")
import Globals
for db in Globals.opened:
try:
db.close()
finally:
pass
def sighandler(signum, frame):
signame = zdaemon.Daemon.get_signal_name(signum)
zLOG.LOG('Z2', zLOG.INFO , "Caught signal %s" % signame)
if signum in [signal.SIGTERM, signal.SIGINT]:
closeall()
zLOG.LOG('Z2', zLOG.INFO , "Shutting down")
sys.exit(0)
if signum == signal.SIGHUP:
closeall()
zLOG.LOG('Z2', zLOG.INFO , "Restarting")
sys.exit(1)
if signum == signal.SIGUSR2:
zLOG.LOG('Z2', zLOG.INFO , "Reopening log files")
if hasattr(sys, '__lg') and hasattr(sys.__lg, 'reopen'):
sys.__lg.reopen()
zLOG.LOG('Z2', zLOG.BLATHER, "Reopened Z2.log")
if (hasattr(sys, '__detailedlog') and
hasattr(sys.__detailedlog, 'reopen')):
zLOG.LOG('Z2', zLOG.BLATHER,"Reopened detailed request log")
sys.__detailedlog.reopen()
if hasattr(zLOG, '_set_stupid_dest'):
zLOG._set_stupid_dest(None)
else:
zLOG._stupid_dest = None
ZLogger.stupidFileLogger._stupid_dest = None
zLOG.LOG('Z2', zLOG.BLATHER, "Reopened event log")
zLOG.LOG('Z2', zLOG.INFO, "Log files reopened successfully")
INTERESTING_SIGNALS = {
signal.SIGTERM: sighandler,
signal.SIGHUP: sighandler,
signal.SIGUSR2: sighandler,
signal.SIGINT: sighandler,
}
def installsighandlers():
# normal kill, restart, and open&close logs respectively
for signum, sighandler in INTERESTING_SIGNALS.items():
signal.signal(signum, sighandler)
signame = zdaemon.Daemon.get_signal_name(signum)
zLOG.LOG('Z2',zLOG.BLATHER,"Installed sighandler for %s" % signame)
######################################################################## ########################################################################
# Configuration section # Configuration section
...@@ -510,7 +579,8 @@ if Zpid and not READ_ONLY: ...@@ -510,7 +579,8 @@ if Zpid and not READ_ONLY:
import zdaemon, App.FindHomes, posix import zdaemon, App.FindHomes, posix
sys.ZMANAGED=1 sys.ZMANAGED=1
zdaemon.run(sys.argv, os.path.join(CLIENT_HOME, Zpid)) zdaemon.run(sys.argv, os.path.join(CLIENT_HOME, Zpid),
INTERESTING_SIGNALS.keys())
os.chdir(CLIENT_HOME) os.chdir(CLIENT_HOME)
...@@ -535,7 +605,12 @@ try: ...@@ -535,7 +605,12 @@ try:
if DETAILED_LOG_FILE: if DETAILED_LOG_FILE:
from ZServer import DebugLogger from ZServer import DebugLogger
logfile=os.path.join(CLIENT_HOME, DETAILED_LOG_FILE) logfile=os.path.join(CLIENT_HOME, DETAILED_LOG_FILE)
DebugLogger.log=DebugLogger.DebugLogger(logfile).log zLOG.LOG('z2', zLOG.BLATHER,
'Using detailed request log file %s' % logfile)
DL=DebugLogger.DebugLogger(logfile)
DebugLogger.log=DL.log
DebugLogger.reopen=DL.reopen
sys.__detailedlog=DL
# Import Zope (or Main) # Import Zope (or Main)
exec "import "+MODULE in {} exec "import "+MODULE in {}
...@@ -579,16 +654,21 @@ try: ...@@ -579,16 +654,21 @@ try:
if READ_ONLY: if READ_ONLY:
lg = logger.file_logger('-') # log to stdout lg = logger.file_logger('-') # log to stdout
zLOG.LOG('z2', zLOG.BLATHER, 'Logging access log to stdout')
elif os.environ.has_key('ZSYSLOG_ACCESS'): elif os.environ.has_key('ZSYSLOG_ACCESS'):
if os.environ.has_key("ZSYSLOG_ACCESS_FACILITY"): if os.environ.has_key("ZSYSLOG_ACCESS_FACILITY"):
lg = logger.syslog_logger(os.environ['ZSYSLOG_ACCESS'],facility=os.environ['ZSYSLOG_ACCESS_FACILITY']) lg = logger.syslog_logger(os.environ['ZSYSLOG_ACCESS'],facility=os.environ['ZSYSLOG_ACCESS_FACILITY'])
else: else:
lg = logger.syslog_logger(os.environ['ZSYSLOG_ACCESS']) lg = logger.syslog_logger(os.environ['ZSYSLOG_ACCESS'])
zLOG.LOG('z2', zLOG.BLATHER, 'Using local syslog access log')
elif os.environ.has_key('ZSYSLOG_ACCESS_SERVER'): elif os.environ.has_key('ZSYSLOG_ACCESS_SERVER'):
(addr, port) = os.environ['ZSYSLOG_ACCESS_SERVER'].split( ':') (addr, port) = os.environ['ZSYSLOG_ACCESS_SERVER'].split( ':')
lg = logger.syslog_logger((addr, int(port))) lg = logger.syslog_logger((addr, int(port)))
zLOG.LOG('z2', zLOG.BLATHER, 'Using remote syslog access log')
else: else:
lg = logger.file_logger(LOG_PATH) lg = logger.file_logger(LOG_PATH)
zLOG.LOG('z2', zLOG.BLATHER, 'Using access log file %s' % LOG_PATH)
sys.__lg = lg
# HTTP Server # HTTP Server
if HTTP_PORT: if HTTP_PORT:
...@@ -786,7 +866,7 @@ try: ...@@ -786,7 +866,7 @@ try:
except: except:
raise raise
# Check umask sanity. # Check umask sanity and install signal handlers if we're on posix.
if os.name == 'posix': if os.name == 'posix':
# umask is silly, blame POSIX. We have to set it to get its value. # umask is silly, blame POSIX. We have to set it to get its value.
current_umask = os.umask(0) current_umask = os.umask(0)
...@@ -796,6 +876,8 @@ try: ...@@ -796,6 +876,8 @@ try:
zLOG.LOG("z2", zLOG.INFO, 'Your umask of ' + current_umask + \ zLOG.LOG("z2", zLOG.INFO, 'Your umask of ' + current_umask + \
' may be too permissive; for the security of your ' + \ ' may be too permissive; for the security of your ' + \
'Zope data, it is recommended you use 077') 'Zope data, it is recommended you use 077')
# we've deferred til now to actuall install signal handlers
installsighandlers()
except: except:
# Log startup exception and tell zdaemon not to restart us. # Log startup exception and tell zdaemon not to restart us.
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment