source: subversion/applications/utils/tirex/bin/tirex-master @ 21037

Last change on this file since 21037 was 21002, checked in by jochen, 10 years ago

code cleanup

  • Property svn:executable set to *
File size: 22.0 KB
Line 
1#!/usr/bin/perl
2#-----------------------------------------------------------------------------
3#
4#  Tirex Tile Rendering System
5#
6#  tirex-master
7#
8#-----------------------------------------------------------------------------
9#  See end of this file for documentation.
10#-----------------------------------------------------------------------------
11#
12#  Copyright (C) 2010  Frederik Ramm <frederik.ramm@geofabrik.de> and
13#                      Jochen Topf <jochen.topf@geofabrik.de>
14
15#  This program is free software; you can redistribute it and/or
16#  modify it under the terms of the GNU General Public License
17#  as published by the Free Software Foundation; either version 2
18#  of the License, or (at your option) any later version.
19
20#  This program is distributed in the hope that it will be useful,
21#  but WITHOUT ANY WARRANTY; without even the implied warranty of
22#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
23#  GNU General Public License for more details.
24
25#  You should have received a copy of the GNU General Public License
26#  along with this program; If not, see <http://www.gnu.org/licenses/>.
27#
28#-----------------------------------------------------------------------------
29
30use strict;
31use warnings;
32
33use Getopt::Long qw( :config gnu_getopt );
34use IO::Socket;
35use IO::Select;
36use Socket;
37use Fcntl;
38use Sys::Syslog;
39use POSIX 'setsid';
40use Pod::Usage;
41
42use Data::Dumper;
43
44use Tirex;
45use Tirex::Queue;
46use Tirex::Manager;
47use Tirex::Source;
48use Tirex::Status;
49use Tirex::Renderer;
50use Tirex::Map;
51
52#-----------------------------------------------------------------------------
53
54die('refusing to run as root') if ($< == 0);
55
56my $started = time();
57
58#-----------------------------------------------------------------------------
59# Reading command line and config
60#-----------------------------------------------------------------------------
61
62my %opts = ();
63GetOptions( \%opts, 'help|h', 'debug|d', 'config|c=s' ) or exit(2);
64
65if ($opts{'help'})
66{
67    pod2usage(
68        -verbose => 1,
69        -msg     => "tirex-master - tirex master daemon\n",
70        -exitval => 0
71    );
72}
73
74if ($opts{'debug'})
75{
76    $Tirex::DEBUG = $opts{'debug'};
77}
78
79my $config_file = ($opts{'config'} || $Tirex::TIREX_CONFIGDIR) . '/' . $Tirex::TIREX_CONFIGFILENAME;
80Tirex::Config::init($config_file);
81
82#-----------------------------------------------------------------------------
83# Initialize logging
84#-----------------------------------------------------------------------------
85
86openlog('tirex-master', $Tirex::DEBUG ? 'pid|perror' : 'pid', 
87    Tirex::Config::get('master_syslog_facility', $Tirex::MASTER_SYSLOG_FACILITY, qr{^(daemon|syslog|user|local[0-7])$}));
88
89syslog('info', 'Tirex master started');
90Tirex::Config::dump_to_syslog();
91
92#-----------------------------------------------------------------------------
93# Read renderer and map config
94#-----------------------------------------------------------------------------
95read_renderer_and_map_config();
96
97foreach my $renderer (Tirex::Renderer->all()) {
98    syslog('info', $renderer->to_s());
99}
100
101foreach my $map (Tirex::Map->all()) {
102    syslog('info', $map->to_s());
103}
104
105#-----------------------------------------------------------------------------
106# Prepare sockets
107#-----------------------------------------------------------------------------
108
109my $want_read  = IO::Select->new();
110my $want_write = IO::Select->new();
111
112# Set up the master socket. This is where we receive render requests using
113# the Tirex protocol.
114# This is a datagram socket.
115my $master_udp_port = Tirex::Config::get('master_udp_port', $Tirex::MASTER_UDP_PORT, qr{^[1-9][0-9]{1,4}$});
116my $master_socket = IO::Socket::INET->new(
117    LocalAddr => 'localhost',
118    LocalPort => $master_udp_port,
119    Proto     => 'udp',
120) or die("Can't open master UDP socket: :$!\n");
121
122$want_read->add([$master_socket, undef]);
123syslog('info', 'Listening for commands on port %d/UDP', $master_udp_port);
124
125# Set up the mod_tile socket. This is where mod_tile requests come in.
126# This is a stream socket.
127my $modtile_socket_name = Tirex::Config::get('modtile_socket_name', $Tirex::MODTILE_SOCK);
128unlink($modtile_socket_name); # ignore return code, opening the socket will fail if something was wrong here
129my $modtile_socket = IO::Socket::UNIX->new(
130    Type     => SOCK_STREAM,
131    Listen   => 1,
132    Local    => $modtile_socket_name,
133    Blocking => 0,
134) or die("Can't open modtile socket: :$!\n");
135chmod($Tirex::MODTILE_PERM, $modtile_socket_name) or die("can't chmod socket '$modtile_socket_name': $!");
136$want_read->add([$modtile_socket, undef]);
137syslog('info', 'Listening for mod_tile connections on %s (UNIX)', $modtile_socket_name);
138
139my $to_syncd;
140if (Tirex::Config::get('sync_to_host')) {
141    my $syncd_udp_port = Tirex::Config::get('syncd_udp_port', $Tirex::SYNCD_UDP_PORT, qr{^[1-9][0-9]{1,4}$});
142    $to_syncd = Socket::pack_sockaddr_in( $syncd_udp_port, Socket::inet_aton('localhost') );
143}
144
145#-----------------------------------------------------------------------------
146# Daemonize and create pidfile
147#-----------------------------------------------------------------------------
148if (!$Tirex::DEBUG)
149{
150    chdir '/' or die "Can't chdir to /: $!";
151    open(STDIN, '<', '/dev/null') or die "Cannot read /dev/null: $!";
152    open(STDOUT, '>', '/dev/null') or die "Cannot write to /dev/null: $!";
153    defined(my $pid = fork) or die "Cannot fork: $!";
154    exit(0) if $pid;
155    setsid() or die "Cannot start a new session: $!";
156    open(STDERR, '>&STDOUT') or die "Cannot dup stdout: $!";
157}
158
159my $pidfile = Tirex::Config::get('master_pidfile', $Tirex::MASTER_PIDFILE);
160open(my $pidfh, '>', $pidfile) or syslog('err', "Can't open pidfile '$pidfile' for writing: $!\n");
161print $pidfh "$$\n";
162close($pidfh);
163
164#-----------------------------------------------------------------------------
165# Initialize status, queue, and rendering manager
166#-----------------------------------------------------------------------------
167my $status = Tirex::Status->new(master => 1);
168
169my $queue = Tirex::Queue->new();
170my $rendering_manager = Tirex::Manager->new( queue => $queue );
171foreach my $bucket_config (@{Tirex::Config::get('bucket')})
172{
173    $rendering_manager->add_bucket(%$bucket_config);
174}
175
176# Set up the renderd return socket. This is where the render daemon notifies
177# us when something is complete.
178# This is a datagram socket.
179my $renderd_return_socket = $rendering_manager->get_socket();
180$want_read->add([$renderd_return_socket, undef]);
181syslog('info', 'Listening for renderd responses');
182
183#-----------------------------------------------------------------------------
184# Set signal handler
185#-----------------------------------------------------------------------------
186
187$SIG{TERM} = \&sigterm_handler; 
188$SIG{INT}  = \&sigint_handler; 
189$SIG{HUP}  = \&sighup_handler; 
190
191# run main loop in eval() since Perl socket operations are notorious for
192# calling croak() when unhappy; if they do, we want to know what happened.
193
194eval { main_loop() };
195if ($@)
196{
197    syslog('crit', $@);
198    die($@);
199}
200
201#-----------------------------------------------------------------------------
202# The big loop
203#-----------------------------------------------------------------------------
204
205sub main_loop
206{
207    my $select_timeout = 1;
208
209    my $accept_timeout = 10;
210    my $read_timeout   = 10;
211    my $write_timeout  = 10;
212
213    my $last_status_update = 0;
214
215
216    while (1) 
217    {
218        my $now = time();
219
220        # keep status in shared memory updated once per second
221        if ($last_status_update < $now)
222        {
223            $status->update(started => $started, queue => $queue->status(), rm => $rendering_manager->status(), renderers => Tirex::Renderer->status(), maps => Tirex::Map->status());
224            $last_status_update = $now;
225        }
226
227        # clean out closed handles
228        foreach my $handle ($want_read->handles()) {
229            my ($socket, $source) = @$handle;
230            my $fcn = $socket->fcntl(F_GETFD, 0);
231            if (!defined($fcn))
232            {
233                syslog('warning', "dropping closed fh %d from want_read (internal state: %s)",
234                   $socket->fileno(), $socket->opened() ? "opened" : "closed");
235                $want_read->remove($handle);
236            }
237        }
238        foreach my $handle ($want_write->handles()) {
239            my ($socket, $source) = @$handle;
240            my $fcn = $socket->fcntl(F_GETFD, 0);
241            if (!defined($fcn))
242            {
243                syslog('warning', "dropping closed fh %d from want_write (internal state: %s)",
244                   $socket->fileno(), $socket->opened() ? "opened" : "closed");
245                $want_write->remove($handle);
246            }
247        }
248
249        my ($readable, $writable, $dummy) = IO::Select::select($want_read, $want_write, undef, $select_timeout);
250
251        # first process all readable handles.
252        # each handle is an array reference, the first element of which is
253        # the socket, and we use the second element to point to a Source object
254        # where appropriate.
255        foreach my $handle (@$readable) 
256        {
257            my ($sock, $source) = @$handle;
258
259            if ($sock == $master_socket)
260            {
261                my $source = Tirex::Source::Command->new( socket => $master_socket );
262                $source->readable($sock);
263                syslog('debug', "got msg from ". $sock->peerport(). ": ". join(' ', map { "$_=$source->{$_}" } sort(keys %$source))) if ($Tirex::DEBUG);
264                my $msg_type = $source->get_msg_type();
265                if ($msg_type eq 'metatile_enqueue_request') {
266                    my $job = $source->make_job();
267                    if ($job)
268                    {
269                        if (my $already_rendering_job = $rendering_manager->requests_by_metatile($job))
270                        {
271                            $already_rendering_job->add_notify($source);
272                        }
273                        else
274                        {
275                            $queue->add($job);
276                        }
277                    }
278                } elsif ($msg_type eq 'metatile_remove_request') {
279                    my $job = $source->make_job();
280                    $queue->remove($job);
281                } elsif ($msg_type eq 'ping') {
282                    syslog('info', 'got ping request');
283                    $source->reply({ type => $msg_type, result => 'ok' });
284                } elsif ($msg_type eq 'reset_max_queue_size') {
285                    syslog('info', 'got reset_max_queue_size message');
286                    $queue->reset_maxsize();
287                    $source->reply({ type => $msg_type, result => 'ok' });
288                } elsif ($msg_type eq 'quit') {
289                    syslog('info', 'got quit message, shutting down now');
290                    $source->reply({ type => 'quit', result => 'ok' });
291                    cleanup();
292                    exit(0);
293                } elsif ($msg_type eq 'debug') {
294                    syslog('info', 'got debug message, activating debug mode');
295                    $source->reply({ type => 'debug', result => 'ok' });
296                    $Tirex::DEBUG=1;
297                } elsif ($msg_type eq 'nodebug') {
298                    syslog('info', 'got nodebug message, deactivating debug mode');
299                    $source->reply({ type => 'nodebug', result => 'ok' });
300                    $Tirex::DEBUG=0;
301                } elsif ($msg_type eq 'stop_rendering_bucket') {
302                    syslog('info', 'got stop_rendering_bucket message');
303                    my $res = defined($source->{'bucket'}) ? $rendering_manager->set_active_flag_on_bucket(0, $source->{'bucket'}) : undef;
304                    $source->reply({ type => $msg_type, result => $res ? 'ok' : 'error_bucket_not_found' });
305                } elsif ($msg_type eq 'continue_rendering_bucket') {
306                    syslog('info', 'got continue_rendering_bucket message');
307                    my $res = defined($source->{'bucket'}) ? $rendering_manager->set_active_flag_on_bucket(1, $source->{'bucket'}) : undef;
308                    $source->reply({ type => $msg_type, result => $res ? 'ok' : 'error_bucket_not_found' });
309                } elsif ($msg_type eq 'shutdown') {
310                    syslog('info', 'got shutdown message, shutting down cleanly');
311                    $source->reply({ type => 'shutdown', result => 'error_not_implemented' });
312#XXX
313                } else {
314                    syslog('warning', "ignoring unknown message type '$msg_type' from command socket" );
315                    $source->reply({ type => $msg_type, result => 'error_unknown_message_type' });
316                }
317            }
318            elsif ($sock == $renderd_return_socket)
319            {
320                my $buf;
321                my $peer = $sock->recv($buf, $Tirex::MAX_PACKET_SIZE);
322                my $msg = Tirex::parse_msg($buf);
323                my $job = $rendering_manager->done($msg);
324                if ($job)
325                {
326                    log_job($job);
327                    $job->notify();
328                    $sock->send($buf, undef, $to_syncd) if ($to_syncd);
329                }
330            }
331            elsif ($sock == $modtile_socket)
332            {
333                # readability on a stream socket means we can accept, but not
334                # necessarily read.
335                my $newsock = $sock->accept();
336                if (!$newsock)
337                {
338                    syslog('err', "could not accept() from mod_tile: $!");
339                }
340                else
341                {
342                    syslog('debug', 'connection from mod_tile accepted') if ($Tirex::DEBUG);
343                    $newsock->blocking(0);
344                    my $source = Tirex::Source::ModTile->new($newsock);
345                    $want_read->add([$newsock, $source]);
346                    $source->set_timeout($now + $accept_timeout);
347                }
348            }
349            else 
350            {
351                # must be one of the mod_tile sockets that we accepted then;
352                # notify the source that it may read something.
353                my $status = $source->readable($sock);
354                $source->set_timeout($now + $read_timeout);
355                if ($status == Tirex::Source::STATUS_MESSAGE_COMPLETE)
356                {
357                    # source returns true, this indicates it doesn't want to
358                    # read more and can prepare a job
359                    # $want_read->remove([$sock]); -- leave in select so we get notified when other side closes
360                    $source->set_timeout($now + 86400);
361                    my $job = $source->make_job($sock);
362                    if (!defined($job))
363                    {
364                        $want_read->remove([$sock]);
365                        $sock->close();
366                        next;
367                    }
368                    my $already_rendering_job = $rendering_manager->requests_by_metatile($job);
369                    if ($job->has_notify())
370                    {           
371                        # give source a chance to re-insert itself into our write
372                        # queue later. slightly inelegant, should rather hand over
373                        # reference to self but we're not an object
374                        $source->set_request_write_callback( sub {
375                            if ($sock->opened)
376                            {
377                                $want_read->remove([$sock, $source]);
378                                $want_write->add([$sock, $source]);
379                                $source->set_timeout(time() + $write_timeout);
380                            }
381                        });
382                        # the following serves the sole purpose of keeping Perl from
383                        # garbage-collecting our socket...
384                        # fixme: respect timeout if specified in request
385                        $already_rendering_job->add_notify($source) if defined($already_rendering_job);
386                    }
387                    else
388                    {
389                        # drop the connection if notification has not been requested
390                        $want_read->remove([$sock]);
391                        $sock->close();
392                    }
393
394                    unless (defined($already_rendering_job))
395                    {
396                        $queue->add($job);
397                    }
398                }
399                elsif ($status == Tirex::Source::STATUS_SOCKET_CLOSED)
400                {
401                    syslog('debug', 'other side closed mod_tile socket %d',
402                        $sock->fileno) if ($Tirex::DEBUG);
403                    $want_read->remove([$sock]);
404                    $sock->close();
405                }
406            }
407        }
408
409        # now handle writes. currently the UDP based writing is done elsewhere, but
410        # the Unix domain socket writing needs to be part of the select loop.
411        foreach my $handle (@$writable) 
412        {
413            my ($sock, $source) = @$handle;
414            my $status= $source->writable($sock);
415            if ($status == Tirex::Source::STATUS_MESSAGE_COMPLETE)
416            {
417                # source is done writing, and the socket goes back to want-read
418                # mode
419                syslog('debug', 'sent answer on mod_tile socket %d',
420                    $sock->fileno) if ($Tirex::DEBUG);
421                $want_write->remove([$sock]);
422                $want_read->add([$sock, $source]);
423                $source->set_timeout($now + $read_timeout);
424            }
425            elsif ($status == Tirex::Source::STATUS_SOCKET_CLOSED)
426            {
427                $want_write->remove([$sock]);
428                $sock->close();
429            }
430            else
431            {
432                # keep on writing
433                $source->set_timeout($now + $write_timeout);
434            }
435        }
436
437        foreach my $handle ($want_write->handles)
438        {
439            my ($socket, $source) = @$handle;
440            next unless defined($source); # udp sockets don't have source
441            if ($source->get_timeout() < $now && $source->get_timeout() > 0)
442            {
443                # timeout on writing the result. close.
444                syslog('warning', 'timeout writing to socket %d; discarding response', $socket->fileno);
445                $want_write->remove([$socket]);
446                $want_read->remove([$socket]);
447                $socket->close();
448                $source->set_timeout(0); # avoid tight loop in case of problems
449            }
450        }
451
452        foreach my $handle ($want_read->handles)
453        {
454            my ($socket, $source) = @$handle;
455            next unless defined($source); # udp sockets don't have source
456            if ($source->get_timeout() < $now && $source->get_timeout() > 0)
457            {
458                # timeout on reading a request. close.
459                syslog('warning', 'timeout reading from socket %d; closing connection', $socket->fileno);
460                $want_read->remove([$socket]);
461                $socket->close();
462                $source->set_timeout(0); # avoid tight loop in case of problems
463            }
464        }
465
466        $rendering_manager->schedule(); # processes anything added
467    }
468}
469
470#-----------------------------------------------------------------------------
471
472sub log_job
473{
474    my $job = shift;
475    my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time);
476
477    my $logfile = Tirex::Config::get('master_logfile', $Tirex::MASTER_LOGFILE);
478    if (open(my $logfh, '>>', $logfile))
479    {
480        printf $logfh "%04d-%02d-%02dT%02d:%02d:%02d id=%s map=%s x=%d y=%d z=%d prio=%d request_time=%s expire=%s sources=%s render_time=%d success=%s\n",
481            1900+$year, 1+$mon, $mday, $hour, $min, $sec,
482            $job->get_id(),
483            $job->get_map(),
484            $job->get_x(),
485            $job->get_y(),
486            $job->get_z(),
487            $job->get_prio(),
488            $job->{'request_time'},
489            defined $job->{'expire'} ? $job->{'expire'} : '',
490            $job->sources_as_string(),
491            $job->{'render_time'},
492            $job->get_success();
493        close($logfh);
494    }
495    else
496    {
497        syslog('err', "Can't write to logfile '$logfile': $!");
498    }
499}
500
501#-----------------------------------------------------------------------------
502sub read_renderer_and_map_config
503{
504    my $dir = $opts{'config'} || $Tirex::TIREX_CONFIGDIR;
505
506    foreach my $file (glob("$dir/renderer/*"))
507    {
508        Tirex::Renderer->new_from_configfile($file);
509    }
510
511    foreach my $file (glob("$dir/maps/*"))
512    {
513        Tirex::Map->new_from_configfile($file);
514    }
515}
516
517
518#-----------------------------------------------------------------------------
519
520# clean up sockets, files, shm
521sub cleanup
522{
523    defined($rendering_manager) && $rendering_manager->log_stats();
524    unlink($modtile_socket_name);
525    unlink($pidfile);
526    $status->destroy();
527}
528
529sub sigint_handler
530{
531    syslog('info', 'SIGINT (CTRL-C) received');
532    cleanup();
533    exit(0);
534}
535
536sub sigterm_handler 
537{
538    syslog('info', 'SIGTERM received');
539    cleanup();
540    exit(0);
541}
542
543sub sighup_handler
544{
545    syslog('info', 'SIGHUP received');
546# XXX ignore for the time being
547    $SIG{HUP} = \&sighup_handler;
548}
549
550__END__
551
552=head1 NAME
553
554tirex-master - tirex master daemon
555
556=head1 SYNOPSIS
557
558tirex-master [OPTIONS]
559
560=head1 OPTIONS
561
562=over 8
563
564=item B<-h>, B<--help>
565
566Display help message.
567
568=item B<-d>, B<--debug>
569
570Run in debug mode. You'll see the actual messages sent and received.
571
572=item B<-c>, B<--config=DIR>
573
574Use the config directory DIR instead of /etc/tirex.
575
576=back
577
578=head1 DESCRIPTION
579
580The tirex master process gets requests for metatiles from mod_tile and other
581sources, queues them and sends them on to tirex-renderd processes. It has a
582sophisticated queueing and rendering manager that decides what is to be
583rendered when.
584
585Unless the --debug option is given, tirex-master will detach from the
586terminal.
587
588=head1 FILES
589
590=over 8
591
592=item F</etc/tirex/tirex.conf>
593
594The configuration file. See tirex.conf(5) for further details.
595
596=item F</etc/tirex/renderer/*>
597
598The renderer configuration files.
599
600=item F</etc/tirex/maps/*>
601
602The map configuration files.
603
604=item F</var/log/tirex/master.log>
605
606Default location for master logfile. It contains one line for each
607metatile rendered.
608
609=back
610
611=head1 DIAGNOSTICS
612
613=head1 AUTHORS
614
615Frederik Ramm <frederik.ramm@geofabrik.de>, Jochen Topf
616<jochen.topf@geofabrik.de> and possibly others.
617
618=cut
619
620
621#-- THE END ------------------------------------------------------------------
Note: See TracBrowser for help on using the repository browser.