source: subversion/applications/utils/mod_tile/mod_tile.c @ 4955

Last change on this file since 4955 was 4955, checked in by jonb, 12 years ago

mod_tile: Apache module and rendering daemon for serving OSM tiles

File size: 13.3 KB
Line 
1#include "apr.h"
2#include "apr_strings.h"
3#include "apr_thread_proc.h"    /* for RLIMIT stuff */
4#include "apr_optional.h"
5#include "apr_buckets.h"
6#include "apr_lib.h"
7#include "apr_poll.h"
8
9#define APR_WANT_STRFUNC
10#define APR_WANT_MEMFUNC
11#include "apr_want.h"
12
13#include "util_filter.h"
14#include "ap_config.h"
15#include "httpd.h"
16#include "http_config.h"
17#include "http_request.h"
18#include "http_core.h"
19#include "http_protocol.h"
20#include "http_main.h"
21#include "http_log.h"
22#include "util_script.h"
23#include "ap_mpm.h"
24#include "mod_core.h"
25#include "mod_cgi.h"
26
27module AP_MODULE_DECLARE_DATA tile_module;
28
29#include <stdio.h>
30#include <stdlib.h>
31#include <string.h>
32#include <stdarg.h>
33#include <sys/types.h>
34#include <sys/stat.h>
35#include <sys/socket.h>
36#include <sys/un.h>
37#include <unistd.h>
38#include <fcntl.h>
39#include <errno.h>
40#include <limits.h>
41#include <time.h>
42
43#include "gen_tile.h"
44#include "protocol.h"
45
46#define MAX_ZOOM 18
47// MAX_SIZE is the biggest file which we will return to the user
48#define MAX_SIZE (1 * 1024 * 1024)
49// IMG_PATH must have blank.png etc.
50#define WWW_ROOT "/var/www/html"
51#define IMG_PATH "/img"
52// TILE_PATH must have tile z directory z(0..18)/x/y.png
53#define TILE_PATH "/osm_tiles2"
54//#define TILE_PATH "/tile"
55// MAX_LOAD_OLD: if tile is out of date, don't re-render it if past this load threshold (users gets old tile)
56#define MAX_LOAD_OLD 5
57// MAX_LOAD_OLD: if tile is missing, don't render it if past this load threshold (user gets 404 error)
58#define MAX_LOAD_MISSING 10
59// MAX_LOAD_ANY: give up serving any data if beyond this load (user gets 404 error)
60#define MAX_LOAD_ANY 100
61// Maximum tile age in seconds
62// TODO: this mechanism should really be a hard cutoff on planet update time.
63#define MAX_AGE (48 * 60 * 60)
64
65// Typical interval between planet imports, used as basis for tile expiry times
66#define PLANET_INTERVAL (7 * 24 * 60 * 60)
67
68// Planet import should touch this file when complete
69#define PLANET_TIMESTAMP "/tmp/planet-import-complete"
70
71// Timeout before giving for a tile to be rendered
72#define REQUEST_TIMEOUT (3)
73#define FD_INVALID (-1)
74
75
76#define MIN(x,y) ((x)<(y)?(x):(y))
77#define MAX(x,y) ((x)>(y)?(x):(y))
78
79
80enum tileState { tileMissing, tileOld, tileCurrent };
81
82static int error_message(request_rec *r, const char *format, ...)
83                 __attribute__ ((format (printf, 2, 3)));
84
85static int error_message(request_rec *r, const char *format, ...)
86{
87    va_list ap;
88    va_start(ap, format);
89    int len;
90    char *msg;
91
92    len = vasprintf(&msg, format, ap);
93
94    if (msg) {
95        ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "%s", msg);
96        r->content_type = "text/plain";
97        if (!r->header_only)
98            ap_rputs(msg, r);
99        free(msg);
100    }
101
102    return OK;
103}
104
105
106int socket_init(request_rec *r)
107{
108    const char *spath = RENDER_SOCKET;
109    int fd;
110    struct sockaddr_un addr;
111
112    //fprintf(stderr, "Starting rendering client\n");
113
114    fd = socket(PF_UNIX, SOCK_STREAM, 0);
115    if (fd < 0) {
116        ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "failed to create unix socket");
117        return FD_INVALID;
118    }
119
120    bzero(&addr, sizeof(addr));
121    addr.sun_family = AF_UNIX;
122    strncpy(addr.sun_path, spath, sizeof(addr.sun_path));
123
124    if (connect(fd, (struct sockaddr *) &addr, sizeof(addr)) < 0) {
125        ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "socket connect failed for: %s", spath);
126        close(fd);
127        return FD_INVALID;
128    }
129    return fd;
130}
131
132
133int request_tile(request_rec *r, int x, int y, int z, const char *filename, int dirtyOnly)
134{
135    struct protocol cmd;
136    //struct pollfd fds[1];
137    static int fd = FD_INVALID;
138    int ret = 0;
139
140    if (fd == FD_INVALID) {
141        fd = socket_init(r);
142
143        if (fd == FD_INVALID) {
144            //fprintf(stderr, "Failed to connect to renderer\n");
145            return 0;
146        } else {
147        ap_log_rerror(APLOG_MARK, APLOG_INFO, 0, r, "Connected to renderer");
148        }
149    }
150
151    bzero(&cmd, sizeof(cmd));
152
153    cmd.ver = PROTO_VER;
154    cmd.cmd = dirtyOnly ? cmdDirty : cmdRender;
155    cmd.z = z;
156    cmd.x = x;
157    cmd.y = y;
158    strcpy(cmd.path, filename);
159
160    //fprintf(stderr, "Requesting tile(%d,%d,%d)\n", z,x,y);
161    ret = send(fd, &cmd, sizeof(cmd), 0);
162    if (ret != sizeof(cmd)) {
163        if (errno == EPIPE) {
164            close(fd);
165            fd = FD_INVALID;
166        }
167
168        //perror("send error");
169        return 0;
170    }
171    if (!dirtyOnly) {
172        struct timeval tv = { REQUEST_TIMEOUT, 0 };
173        fd_set rx;
174        int s;
175
176        while (1) {
177            FD_ZERO(&rx);
178            FD_SET(fd, &rx);
179            s = select(fd+1, &rx, NULL, NULL, &tv);
180            if (s == 1) {
181                bzero(&cmd, sizeof(cmd));
182                ret = recv(fd, &cmd, sizeof(cmd), 0);
183                if (ret != sizeof(cmd)) {
184                    if (errno == EPIPE) {
185                        close(fd);
186                        fd = FD_INVALID;
187                    }
188                    //perror("recv error");
189                    break;
190                }
191                //fprintf(stderr, "Completed tile(%d,%d,%d)\n", z,x,y);
192                if (cmd.x == x && cmd.y == y && cmd.z == z) {
193                    if (cmd.cmd == cmdDone)
194                        return 1;
195                    else
196                        return 0;
197                }
198            } else if (s == 0) {
199                break;
200            } else {
201                if (errno == EPIPE) {
202                    close(fd);
203                    fd = FD_INVALID;
204                    break;
205                }
206            }
207        }
208    }
209    return 0;
210}
211
212static int getPlanetTime(request_rec *r)
213{
214    static time_t last_check;
215    static time_t planet_timestamp;
216    time_t now = time(NULL);
217    struct stat buf;
218
219    // Only check for updates periodically
220    if (now < last_check + 300)
221        return planet_timestamp;
222
223    last_check = now;
224    if (stat(PLANET_TIMESTAMP, &buf)) {
225        ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Planet timestamp file " PLANET_TIMESTAMP " is missing");
226        // Make something up
227        planet_timestamp = now - 3 * 24 * 60 * 60;
228    } else {
229        if (buf.st_mtime != planet_timestamp) {
230            ap_log_rerror(APLOG_MARK, APLOG_INFO, 0, r, "Planet file updated at %s", ctime(&buf.st_mtime));
231            planet_timestamp = buf.st_mtime;
232        }
233    }
234    return planet_timestamp;
235}
236
237enum tileState tile_state(request_rec *r, const char *filename)
238{
239    // FIXME: Apache already has most, if not all, this info recorded in r->fileinfo, use this instead!
240    struct stat buf;
241
242    if (stat(filename, &buf))
243        return tileMissing; 
244
245    if (buf.st_mtime < getPlanetTime(r))
246        return tileOld;
247
248    return tileCurrent;
249}
250
251static apr_status_t expires_filter(ap_filter_t *f, apr_bucket_brigade *b)
252{
253    request_rec *r = f->r;
254    apr_time_t expires, holdoff, nextPlanet;
255    apr_table_t *t = r->headers_out;
256    enum tileState state = tile_state(r, r->filename);
257    char *timestr;
258
259    /* Append expiry headers ... */
260
261    //ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "expires(%s), uri(%s), filename(%s), path_info(%s)\n",
262    //              r->handler, r->uri, r->filename, r->path_info);
263
264    // current tiles will expire after next planet dump is due
265    // or after 1 hour if the planet dump is late or tile is due for re-render
266    nextPlanet = (state == tileCurrent) ? apr_time_from_sec(getPlanetTime(r) + PLANET_INTERVAL) : 0;
267    holdoff = r->request_time + apr_time_from_sec(60 * 60);
268    expires = MAX(holdoff, nextPlanet);
269
270    apr_table_mergen(t, "Cache-Control",
271                     apr_psprintf(r->pool, "max-age=%" APR_TIME_T_FMT,
272                     apr_time_sec(expires - r->request_time)));
273    timestr = apr_palloc(r->pool, APR_RFC822_DATE_LEN);
274    apr_rfc822_date(timestr, expires);
275    apr_table_setn(t, "Expires", timestr);
276
277    ap_remove_output_filter(f);
278    return ap_pass_brigade(f->next, b);
279}
280
281static int serve_blank(request_rec *r)
282{
283    // Redirect request to blank tile
284    r->method = apr_pstrdup(r->pool, "GET");
285    r->method_number = M_GET;
286    apr_table_unset(r->headers_in, "Content-Length");
287    ap_internal_redirect_handler(IMG_PATH "/blank-000000.png", r);
288    return OK;
289}
290
291int get_load_avg(request_rec *r)
292{
293    FILE *loadavg = fopen("/proc/loadavg", "r");
294    int avg = 1000;
295
296    if (!loadavg) {
297        ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "failed to read /proc/loadavg");
298        return 1000;
299    }
300    if (fscanf(loadavg, "%d", &avg) != 1) {
301        ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "failed to parse /proc/loadavg");
302        fclose(loadavg);
303        return 1000;
304    }
305    fclose(loadavg);
306
307    return avg;
308}
309
310static int tile_dirty(request_rec *r, int x, int y, int z, const char *path)
311{
312    request_tile(r, x,y,z,path, 1);
313    return OK;
314}
315
316
317
318
319static int get_tile(request_rec *r, int x, int y, int z, const char *path)
320{
321    int avg = get_load_avg(r);
322    enum tileState state;
323
324    if (avg > MAX_LOAD_ANY) {
325        // we're too busy to send anything now
326        return error_message(r, "error: Load above MAX_LOAD_ANY threshold %d > %d", avg, MAX_LOAD_ANY);
327    }
328
329    state = tile_state(r, path);
330
331    // Note: We rely on the default Apache handler to return the files from the filesystem
332    // hence we return DECLINED in order to return the tile to the client
333    // or OK if we want to send something else.
334
335    switch (state) {
336        case tileCurrent:
337            return DECLINED;
338            break;
339        case tileOld:
340            if (avg > MAX_LOAD_OLD) {
341               // Too much load to render it now, mark dirty but return old tile
342               tile_dirty(r, x, y, z, path);
343               return DECLINED;
344            }
345            break;
346        case tileMissing:
347            if (avg > MAX_LOAD_MISSING) {
348               tile_dirty(r, x, y, z, path);
349               return error_message(r, "error: File missing and load above MAX_LOAD_MISSING threshold %d > %d", avg, MAX_LOAD_MISSING);
350            }
351            break;
352    }
353
354    if (request_tile(r, x,y,z,path, 0)) {
355        // Need to make apache try accessing this tile again (since it may have been missing)
356        // TODO: Instead of redirect, maybe we can update fileinfo for new tile, but is this sufficient?
357        apr_table_unset(r->headers_in, "Content-Length");
358        ap_internal_redirect_handler(r->uri, r);
359        return OK;
360    }
361    return error_message(r, "rendering failed for %s", path);
362}
363
364static int tile_status(request_rec *r, int x, int y, int z, const char *path)
365{
366    // FIXME: Apache already has most, if not all, this info recorded in r->fileinfo, use this instead!
367    struct stat buf;
368    time_t now;
369    int old;
370    char MtimeStr[32]; // At least 26 according to man ctime_r
371    char AtimeStr[32]; // At least 26 according to man ctime_r
372    char *p;
373
374    if (stat(path, &buf))
375        return error_message(r, "Unable to find a tile at %s", path);
376
377    now = time(NULL);
378    old = (buf.st_mtime < now - MAX_AGE);
379
380    MtimeStr[0] = '\0';
381    ctime_r(&buf.st_mtime, MtimeStr);
382    AtimeStr[0] = '\0';
383    ctime_r(&buf.st_atime, AtimeStr);
384
385    if ((p = strrchr(MtimeStr, '\n')))
386        *p = '\0';
387    if ((p = strrchr(AtimeStr, '\n')))
388        *p = '\0';
389
390    return error_message(r, "Tile is %s. Last rendered at %s. Last accessed at %s", old ? "due to be rendered" : "clean", MtimeStr, AtimeStr);
391}
392
393
394static int tile_handler(request_rec *r)
395{
396    int x, y, z, n, limit;
397    char option[11];
398    int oob;
399    char path[PATH_MAX];
400
401    option[0] = '\0';
402
403    if(strcmp(r->handler, "tile"))
404        return DECLINED;
405
406    //ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "handler(%s), uri(%s), filename(%s), path_info(%s)",
407    //              r->handler, r->uri, r->filename, r->path_info);
408
409    /* URI = .../<z>/<x>/<y>.png[/option] */
410    n = sscanf(r->uri, TILE_PATH "/%d/%d/%d.png/%10s", &z, &x, &y, option);
411    if (n < 3) {
412        //return error_message(r, "unable to process: %s", r->path_info);
413        return DECLINED;
414    }
415
416    // Generate the tile filename.
417    // This may differ from r->filename in some cases (e.g. if a parent directory is missing)
418    snprintf(path, PATH_MAX, WWW_ROOT TILE_PATH "/%d/%d/%d.png", z, x, y);
419
420    // Validate tile co-ordinates
421    oob = (z < 0 || z > MAX_ZOOM);
422    if (!oob) {
423         // valid x/y for tiles are 0 ... 2^zoom-1
424        limit = (1 << z) - 1;
425        oob =  (x < 0 || x > limit || y < 0 || y > limit);
426    }
427
428    if (n == 3) {
429        ap_add_output_filter("MOD_TILE", NULL, r, r->connection);
430        return oob ? serve_blank(r) : get_tile(r, x, y, z, path);
431    }
432
433    if (n == 4) {
434        if (oob)
435            return error_message(r, "The tile co-ordinates that you specified are invalid");
436        if (!strcmp(option, "status"))
437            return tile_status(r, x, y, z, path);
438        if (!strcmp(option, "dirty"))
439            return tile_dirty(r, x, y, z, path);
440        return error_message(r, "Unknown option");
441    }
442    return DECLINED;
443}
444
445static void register_hooks(__attribute__((unused)) apr_pool_t *p)
446{
447    ap_register_output_filter("MOD_TILE", expires_filter, NULL, APR_HOOK_MIDDLE);
448    ap_hook_handler(tile_handler, NULL, NULL, APR_HOOK_FIRST);
449}
450
451module AP_MODULE_DECLARE_DATA tile_module =
452{
453    STANDARD20_MODULE_STUFF,
454    NULL,           /* dir config creater */
455    NULL,           /* dir merger --- default is to override */
456    NULL,           /* server config */
457    NULL,           /* merge server config */
458    NULL,           /* command apr_table_t */
459    register_hooks  /* register hooks */
460};
Note: See TracBrowser for help on using the repository browser.