Ahbeyra has asked for the wisdom of the Perl Monks concerning the following question:

Hello fellow monks, I have been experimenting with and working around transfering a set of clients connected to a multiplexing server using IO:Select to the same script, but after it has been restarted. I had the problem initially of it not being able to reopen the port... fixed that by having a second script do the actual restarting of the script...
So, what i have now is a system call from script 1 to script 2, which then calls script 1 again. it seems way out of the way from what i think perl is capable of. that solved one problem none the less, the second problem is actually transfering the clients which are held in a hash, i initially thought that just setting the hash in the new script as the old hash may work... didnt. but i cant figure out how to resocket-ify the clients to the new script. Thats where im lost, i just need to know what would be used to resocket them. here is some of the code to show the contents of the clients hash of hashes and how it was created, hopefully enough to help.
my $ready = (IO::Select->select($select,undef,undef,undef))[0]; foreach my $s (@$ready) { if ($s == $listener) { my $new_sock = $listener->accept; $clients{$new_sock} = { socket => $new_sock, input => '', ip => $new_sock->peerhost }; } }
My ultimate goal is to be able to restart the script with any changes that have been made and keep all the clients from having to log back in.


I love the smell of pastures in the morning... /tell ahbeyra moo

Replies are listed 'Best First'.
Re: Restarting a script without dropping clients
by tachyon (Chancellor) on Dec 15, 2003 at 00:38 UTC

    The standard method is to send the script a SIG HUP (Hang UP) which you catch with a signal handler and do the reininitialization you need. Here is an example I use:

    BEGIN { # allow script to be reinitialized with a HUP signal $SIG{HUP} = sub { $HUP = 1; warn "Caught HUP\n" }; } # main loop while ( 1 ) { $conn = accept_new_conn(); # check if we have been HUPPED if ( $HUP ) { REHUP: # reset the flag $HUP = 0; warn "***** Widget reinitializing\n"; do_disconnect(); do_init(); # now it is possible, but unlikely that we get another SIG HUP + while # we are performing our reinit routines so we need to redo thi +s # stuff if that occurs, otherwise we will be partially reiniti +alised goto REHUP if $HUP; } # more code here }

    In this case I just set a var called $HUP. This is because I have multiple children that get this signal. They are not all active at the one time and all I need them to do is re-read data out of the database. To save useless database hits from children that are sitting inactive waiting for connections I have them check if they have been hupped in their main loop. That way you can send 100 HUPs but if only one child is currently active there will only be one call to the DB, rather than 100 x NUM_KIDS. If a child is active it will perform the required RE_INIT before it completes handling the client connection.

    Note to send a HUP your HUPper will need to be root. In CGIs I handle this by adding this line to /etc/sudoers

    apache ALL=NOPASSWD:/home/www/utility/sendHUP.pl

    This lets me call the sendHUP.pl script via a system call in a CGI and have the required HUP happen without getting involved with suid scripts and all the security issues they entail.

    The sendHUP.pl script forks due mostly to historical reasons as we did have a 2 second delay between HUPping each kid (so they would not all be rininitializing at once) but the delayed HUP as required technique now makes this unnecessary.

    #!/usr/bin/perl -w # sendHUP.pl # this script need to be run as root, to do this we add an entry to # /etc/sudoers so that apache can run it (you edit using visudo) # visudo -f /etc/sudoers # add this line # apache ALL=NOPASSWD:/devel/www/utility/sendHUP.pl # call as system('sudo', '/devel/www/utility/sendHUP.pl'); $|++; #use strict; use Fcntl; use POSIX qw(setsid); # we really really don't want to half HUP the widgets # this can potentially occur so we ignore INT and TERM SIGs BEGIN { $SIG{INT} = $SIG{TERM} = sub { warn $SIG{INT} ? "*****sendHUP +caught INT" : "*****sendHUP caught TERM" } } my $PROGRAM = 'widget.pl'; my @ps = `ps -C $PROGRAM`; @ps = map { m/(\d+)/; $1 } grep { /\Q$PROGRAM\E/ } @ps; # now demonize it defined(my $pid = fork) or die "Can't fork: $!"; exit 0 if $pid; chdir '/' or die "Can't chdir to /: $!"; umask 0; setsid() or die "Can't start a new session: $!"; for ( @ps ) { (kill HUP, $_) or warn "***** Failed to HUP $_\n"; } my $time = gmtime(); warn "[$time] Sent SIGHUP to $PROGRAM @ps\n"; exit 0;

    cheers

    tachyon

Re: Restarting a script without dropping clients
by samtregar (Abbot) on Dec 15, 2003 at 04:36 UTC
    One option is to create a proxy server which allows your code to drop and reopen connections which are transparently connected to the clients. Basically something like:

         server <--> proxy <--> clients

    When you need to restart the server, just send the proxy a special message saying "buffer all client connections until the new server boots". Then kill the server and restart with new code. The server connects to the proxy and resumes talking to the clients who are none the wiser.

    Another, much more drastic, option would be to use UDP sockets instead of TCP sockets. UDP sockets are connectionless and you can restart your server as much as you want without disturbing clients. However, UDP sockets require you to handle retransmission when packets are lost and don't guarantee ordered delievery of packets, so there's definitely a downside here.

    -sam

      Another, much more drastic, option would be to use UDP sockets instead of TCP sockets. UDP sockets are connectionless and you can restart your server as much as you want without disturbing clients. However, UDP sockets require you to handle retransmission when packets are lost and don't guarantee ordered delievery of packets, so there's definitely a downside here.

      Depending on the nature of the application, UDP can also be less secure. All you know is that someone claimed to place something in your UDP conversation. Securing UDP has a much more difficult path than TCP. I am not saying it cannot be done, you just end up writing many of the things that TCP already handles for you.

      --MidLifeXis

Re: Restarting a script without dropping clients
by pg (Canon) on Dec 15, 2003 at 03:12 UTC
    "I had the problem initially of it not being able to reopen the port... fixed that by having a second script do the actual restarting of the script... "

    To ensure that you can reopen port right the way, add this when you create socket:

    reuse => 1

    Maybe you should rethink what you are trying to do, whether you are using a canon to kill a fly?

Re: Restarting a script without dropping clients
by Ahbeyra (Monk) on Dec 15, 2003 at 02:06 UTC
    I think my title may have been a little misleading, though that is very helpful for one of my other projects actually. it doesnt seem like it will reload the script so that new changes are implemented. thats the main thing that is giving me a problem is reloading the script completely. Thanks for that help though. possibly saved me time in the future.

    -Kelley

    I love the smell of pastures in the morning... /tell ahbeyra moo

      You can't KILL a script, start a new instance of that script and pass open TCP socket connections from the deceased script to the new process easily. You can fork of course but all that gives you is a copy of the existing process running at the same point so it does not solve your issue.

      You say reload the script completely but you don't seem to want that. You want to reinitialize some stuff but keep some stuff in the running code. You can start a brand new script (mutating the running one into it) with exec() but I doubt this is what you want.

      If the issue is maintaining a login state then use some sort of persistent session arrangement, almost everyone has written one ;-) but there are lots of good ones on CPAN depending on context.

      An alternative would still be to use the HUP signal but arrange it so that when a process recieves the HUP:

      1. it handles any existing connections
      2. does not accept any new ones
      3. exits when done

      You can then start a new process which will handle new connections while the old process handles the existing ones. This is the basis of the apache graceful command. The major issue you will probably have is that the old process may be bound to a particular listen port and thus prevent the new process from starting......

      cheers

      tachyon

        You can't KILL a script, start a new instance of that script and pass open TCP socket connections from the deceased script to the new process easily.

        Agreed.

        You say reload the script completely but you don't seem to want that. You want to reinitialize some stuff but keep some stuff in the running code.

        Depending on the code, and based on the OP's question, it may be sufficient to reload any modules that have changed on disk, and perhaps also any configuration files.

        Tachyon's earlier post gives a good skeleton for this; what's missing is something to reload any modules that have changed. I think that a hacked copy of Apache::StatINC would do the job -- take the code from handler sub, without the first four lines which are Apache-specific.

        However, note that reloading the code this way is a bit unusual, and some of your code may need to be readjusted a bit to avoid some "gotchas," such as re-initializing package variables, leaving dangling references to old data, or other unexpected surprises.

Re: Restarting a script without dropping clients
by Ahbeyra (Monk) on Dec 15, 2003 at 03:18 UTC
    I was thinking maybe something like taking the client information that was passed on and running them back through the new socket kind of process, but i was uncertain how to go about that with the information that i had saved in the hash. would something like that even make sense to attempt?
    I love the smell of pastures in the morning... /tell ahbeyra moo
Re: Restarting a script without dropping clients
by Ahbeyra (Monk) on Dec 15, 2003 at 03:16 UTC
    Actually that was a stupid mistake, i already have that, but i forgot to tell it to exit the script first. that would make it difficult :)
    I love the smell of pastures in the morning... /tell ahbeyra moo
Re: Restarting a script without dropping clients
by mr_mischief (Monsignor) on Dec 17, 2003 at 21:03 UTC
    Perhaps I'm still missing something after having read the whole thread so far. You don't have to close a filehandle then reopen it when you restart a program.

    You can exec() the same program from itself with all files still open. The tough part becomes knowing how many files you have open.

    Since you have control of the command line upon re-exec()ing your code, you can pass that on the command line. File descriptors can come in handy here. The fdopen(3) action of Perl's open() can make life bearable when needing to use numbers as file descriptors instead of having the file handles from the previous incarnation of the program. You can clone your descriptors back to filehandles as you please.

    Update: Fixed a typo.



    Christopher E. Stith
      Ahbeyra asked me about this idea. TMTOWTDI -- a hard way and an easy way.

      The hard way:

      Now, I should mention I haven't actually tried this in Perl, but it works in C and the Camel documents it...

      The 'close on exec' flag is available through the Fcntl module as 'F_SETFD'. This flag is, at least in Perl, on by default execpt for 0, 1, and 2 (STDIN, STDOUT, and STDERR).

      What you do is clear this flag on all the descriptors you need to keep open, do your exec, pass somehow on the command line which descriptors should be open , then start playing with your still-open files.


      Then there's the easy way...

      The Camel also mentions the $^F variable ( AKA $SYSTEM_FD_MAX ). According to the Camel, "When Perl opens a new file (or pipe or socket), it checks the current setting of the $^F ($SYSTEM_FD_MAX) variable. If the numeric file descriptor used by that new filehandle is greater than $^F, the descriptor is marked as one to close. Otherwise, Perl leaves it alone, and new programs you exec will inherit access."

      So you could find your highest opened descriptor (or just choose an arbitrary very large number) and set $^F. Then, you can open your files, keep track of which are open in a temp file or on the command line for your new instance, and mess with your files via the descriptors in your newly exec()'ed process.

      I hope this clears things up a bit. I think this is functionality that is too often overlooked. Also, I'm not sure if this is available everywhere Perl runs, as it seems to me that it may depend on at least a loosely POSIX-like environment. The Fcntl method does require fcntl() availability on your platform. I have never seen any documentation hinting that the $^F method is similarly limited. Some other monks I'm sure know the answer to that.


      Christopher E. Stith