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

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

mod_tile: Implement directory hashing. Improve reconnection between mod_tile and renderer. Use new Mapnik native code for cutting and saving the tiles in 256 colours. Removes ImageMagick? dependency.

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