summaryrefslogtreecommitdiff
path: root/FS/FS/Daemon/Preforking.pm
blob: 4f3f2be8536867d7664f5c31afdd715cadf285b4 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
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
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
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
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
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
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
package FS::Daemon::Preforking;
use base 'Exporter';

=head1 NAME

FS::Daemon::Preforking - A preforking web server

=head1 SYNOPSIS

  use FS::Daemon::Preforking qw( freeside_init1 freeside_init2 daemon_run );

  my $me = 'mydaemon'; #keep unique among fs daemons, for logfiles etc.

  freeside_init1($me); #daemonize, drop root and connect to freeside

  #do setup tasks which should throw an error to the shell starting the daemon

  freeside_init2($me); #move logging to logfile and disassociate from terminal

  #do setup tasks which will warn/error to the log file, such as declining to
  # run if our config is not in place

  daemon_run(
    'port'           => 5454, #keep unique among fs daemons
    'handle_request' => \&handle_request,
  );

  sub handle_request {
    my $request = shift; #HTTP::Request object

    #... do your thing

    return $response; #HTTP::Response object

  }

=head1 AUTHOR

Based on L<http://www.perlmonks.org/?node_id=582781> by Justin Hawkins

and L<http://poe.perl.org/?POE_Cookbook/Web_Server_With_Forking>

=cut

use warnings;
use strict;

use constant DEBUG         => 0;       # Enable much runtime information.
use constant MAX_PROCESSES => 10;      # Total server process count. XXX conf to increase per-different daemon for busy sites using this (currently the only things using this are freeside-xmlrpcd and freeside-selfservice-xmlrpcd)
#use constant TESTING_CHURN => 0;       # Randomly test process respawning.

use vars qw( @EXPORT_OK $FREESIDE_LOG $SERVER_PORT $user $handle_request );
@EXPORT_OK = qw( freeside_init1 freeside_init2 daemon_run );
$FREESIDE_LOG = '%%%FREESIDE_LOG%%%';

use POE 1.2;                     # Base features.
use POE::Filter::HTTPD;          # For serving HTTP content.
use POE::Wheel::ReadWrite;       # For socket I/O.
use POE::Wheel::SocketFactory;   # For serving socket connections.

use FS::Daemon qw( daemonize1 drop_root logfile daemonize2 );
use FS::UID qw( adminsuidsetup forksuidsetup dbh );

#use FS::TicketSystem;

sub freeside_init1 {
  my $name = shift;

  $user = shift @ARGV or die &usage($name);

  $FS::Daemon::NOSIG = 1;
  $FS::Daemon::PID_NEWSTYLE = 1;
  daemonize1($name);

  POE::Kernel->has_forked(); #daemonize forks...

  drop_root();

  adminsuidsetup($user);
}

sub freeside_init2 {
  my $name = shift;

  logfile("$FREESIDE_LOG/$name.log");

  daemonize2();

}

sub daemon_run {
  my %opt = @_;
  $SERVER_PORT = $opt{port};
  $handle_request = $opt{handle_request};

  #parent doesn't need to hold a DB connection open
  dbh->disconnect;
  undef $FS::UID::dbh;
  undef $RT::Handle;

  server_spawn(MAX_PROCESSES);
  POE::Kernel->run();
  #exit;

}

### Spawn the main server.  This will run as the parent process.

sub server_spawn {
    my ($max_processes) = @_;

    POE::Session->create(
      inline_states => {
        _start         => \&server_start,
        _stop          => \&server_stop,
        do_fork        => \&server_do_fork,
        got_error      => \&server_got_error,
        got_sig_int    => \&server_got_sig_int,
        got_sig_child  => \&server_got_sig_child,
        got_connection => \&server_got_connection,
        _child         => sub { undef },
      },
      heap => { max_processes => MAX_PROCESSES },
    );
}

### The main server session has started.  Set up the server socket and
### bookkeeping information, then fork the initial child processes.

sub server_start {
    my ( $kernel, $heap ) = @_[ KERNEL, HEAP ];

    $heap->{server} = POE::Wheel::SocketFactory->new
      ( BindPort     => $SERVER_PORT,
        SuccessEvent => "got_connection",
        FailureEvent => "got_error",
        Reuse        => "yes",
      );

    $kernel->sig( INT  => "got_sig_int" );
    $kernel->sig( TERM => "got_sig_int" ); #huh

    $heap->{children}   = {};
    $heap->{is_a_child} = 0;

    warn "Server $$ has begun listening on port $SERVER_PORT\n";

    $kernel->yield("do_fork");
}

### The server session has shut down.  If this process has any
### children, signal them to shutdown too.

sub server_stop {
    my $heap = $_[HEAP];
    DEBUG and warn "Server $$ stopped.\n";

    if ( my @children = keys %{ $heap->{children} } ) {
        DEBUG and warn "Server $$ is signaling children to stop.\n";
        kill INT => @children;
    }
}

### The server session has encountered an error.  Shut it down.

sub server_got_error {
    my ( $heap, $syscall, $errno, $error ) = @_[ HEAP, ARG0 .. ARG2 ];
      warn( "Server $$ got $syscall error $errno: $error\n",
        "Server $$ is shutting down.\n",
      );
    delete $heap->{server};
}

### The server has a need to fork off more children.  Only honor that
### request form the parent, otherwise we would surely "forkbomb".
### Fork off as many child processes as we need.

sub server_do_fork {
    my ( $kernel, $heap ) = @_[ KERNEL, HEAP ];

    return if $heap->{is_a_child};

    #my $current_children = keys %{ $heap->{children} };
    #for ( $current_children + 2 .. $heap->{max_processes} ) {
    while (scalar(keys %{$heap->{children}}) < $heap->{max_processes}) {

        DEBUG and warn "Server $$ is attempting to fork.\n";

        my $pid = fork();

        unless ( defined($pid) ) {
            DEBUG and
              warn( "Server $$ fork failed: $!\n",
                "Server $$ will retry fork shortly.\n",
              );
            $kernel->delay( do_fork => 1 );
            return;
        }

        # Parent.  Add the child process to its list.
        if ($pid) {
            $heap->{children}->{$pid} = 1;
            $kernel->sig_child($pid, "got_sig_child");
            next;
        }

        # Child.  Clear the child process list.
        $kernel->has_forked();
        DEBUG and warn "Server $$ forked successfully.\n";
        $heap->{is_a_child} = 1;
        $heap->{children}   = {};

        #freeside db connection, etc.
        forksuidsetup($user);

        #why isn't this needed ala freeside-selfservice-server??
        #FS::TicketSystem->init();

        return;
    }
}

### The server session received SIGINT.  Don't handle the signal,
### which in turn will trigger the process to exit gracefully.

sub server_got_sig_int {
    my ( $kernel, $heap ) = @_[ KERNEL, HEAP ];
    DEBUG and warn "Server $$ received SIGINT/TERM.\n";

    if ( my @children = keys %{ $heap->{children} } ) {
        DEBUG and warn "Server $$ is signaling children to stop.\n";
        kill INT => @children;
    }

    delete $heap->{server};
    $kernel->sig_handled();
}

### The server session received a SIGCHLD, indicating that some child
### server has gone away.  Remove the child's process ID from our
### list, and trigger more fork() calls to spawn new children.

sub server_got_sig_child {
    my ( $kernel, $heap, $child_pid ) = @_[ KERNEL, HEAP, ARG1 ];

    return unless delete $heap->{children}->{$child_pid};

   DEBUG and warn "Server $$ reaped child $child_pid.\n";
   $kernel->yield("do_fork") if exists $_[HEAP]->{server};
}

### The server session received a connection request.  Spawn off a
### client handler session to parse the request and respond to it.

sub server_got_connection {
    my ( $heap, $socket, $peer_addr, $peer_port ) = @_[ HEAP, ARG0, ARG1, ARG2 ];

    DEBUG and warn "Server $$ received a connection.\n";

    POE::Session->create(
      inline_states => {
        _start      => \&client_start,
        _stop       => \&client_stop,
        got_request => \&client_got_request,
        got_flush   => \&client_flushed_request,
        got_error   => \&client_got_error,
        _parent     => sub { 0 },
      },
      heap => {
        socket    => $socket,
        peer_addr => $peer_addr,
        peer_port => $peer_port,
      },
    );

#    # Gracefully exit if testing process churn.
#    delete $heap->{server}
#      if TESTING_CHURN and $heap->{is_a_child} and ( rand() < 0.1 );
}

### The client handler has started.  Wrap its socket in a ReadWrite
### wheel to begin interacting with it.

sub client_start {
    my $heap = $_[HEAP];

    $heap->{client} = POE::Wheel::ReadWrite->new
      ( Handle => $heap->{socket},
        Filter       => POE::Filter::HTTPD->new(),
        InputEvent   => "got_request",
        ErrorEvent   => "got_error",
        FlushedEvent => "got_flush",
      );

    DEBUG and warn "Client handler $$/", $_[SESSION]->ID, " started.\n";
}

### The client handler has stopped.  Log that fact.

sub client_stop {
    DEBUG and warn "Client handler $$/", $_[SESSION]->ID, " stopped.\n";
}

### The client handler has received a request.  If it's an
### HTTP::Response object, it means some error has occurred while
### parsing the request.  Send that back and return immediately.
### Otherwise parse and process the request, generating and sending an
### HTTP::Response object in response.

sub client_got_request {
    my ( $heap, $request ) = @_[ HEAP, ARG0 ];

    DEBUG and
      warn "Client handler $$/", $_[SESSION]->ID, " is handling a request.\n";

    if ( $request->isa("HTTP::Response") ) {
        $heap->{client}->put($request);
        return;
   }

    forksuidsetup($user) unless dbh && dbh->ping;

    my $response = &{ $handle_request }( $request );

    $heap->{client}->put($response);
}

### The client handler received an error.  Stop the ReadWrite wheel,
### which also closes the socket.

sub client_got_error {
    my ( $heap, $operation, $errnum, $errstr ) = @_[ HEAP, ARG0, ARG1, ARG2 ];
    DEBUG and
      warn( "Client handler $$/", $_[SESSION]->ID,
        " got $operation error $errnum: $errstr\n",
        "Client handler $$/", $_[SESSION]->ID, " is shutting down.\n"
      );
    delete $heap->{client};
}

### The client handler has flushed its response to the socket.  We're
### done with the client connection, so stop the ReadWrite wheel.

sub client_flushed_request {
    my $heap = $_[HEAP];
    DEBUG and
      warn( "Client handler $$/", $_[SESSION]->ID,
        " flushed its response.\n",
        "Client handler $$/", $_[SESSION]->ID, " is shutting down.\n"
      );
    delete $heap->{client};
}

sub usage {
  my $name = shift;
  die "Usage:\n\n  freeside-$name user\n";
}

1;