Example #1
0
module.exports.ResponseCache = ks_utils.Class({

    default_options: {
        max_age: 600
    },

    methods: [ 'GET', 'HEAD' ],
    ERR_NOT_MODIFIED: 'NOT_MODIFIED',
    ERR_STALE: 'STALE',
    ERR_MISS: 'MISS',

    initialize: function (options) {
        // Create a memcache instance, if necessary
        if (this.options.memcache) {
            var mo = this.options.memcache;
            this.memcached = new Memcached(mo.server, mo.options || {});
        } else {
            // If the configuration is missing, use the fake stub cache
            this.memcached = new ks_utils.FakeMemcached();
        }
    },

    // ### cacheResponse
    //
    // Wrapper for a response handler. Will cache fresh responses from the
    // handler, and skip calling the handler altogether if the cache is valid.
    //
    // Honors HTTP cache control and conditional GET semantics.
    //
    // This kind of wants to be connect/express middleware, but it doesn't
    // *quite* work that way.
    //
    cacheResponse: function (req, res, options, response_cb) {
        var $this = this;

        // Shortcircuit on unsupported request methods
        var method = req.method.toUpperCase(),
            supported_methods = $this.methods;
        if (supported_methods.indexOf(method) === -1) {
            return $this.revalidate(null, {}, req, res, null, response_cb);
        }
        
        // Start building a cache key with the URL path.
        var cache_key_parts = [
            url.parse(req.url).pathname
        ];

        // Include the values of request headers specified in the Vary:
        // response header.
        var vary_names = (''+res.header('vary')).split(',')
                            .map(function (name) {
                                return name.trim().toLowerCase();
                            });
        vary_names.sort();
        vary_names.forEach(function (name) {
            cache_key_parts.push(name + ': ' + req.header(name));
        });

        // Build a cache key based on all the parts.
        var cache_key = 'response-cache:' + crypto.createHash('sha1')
            .update(cache_key_parts.join('|')).digest('hex');
         
        // Handy for diagnostics, maybe not needed
        res.header('X-Cache-Key', cache_key);

        var ua_cc = parseCacheControl(req.headers['cache-control'] || '');
        var ims = req.header('if-modified-since');

        var opts = {
            if_none_match: req.header('if-none-match'),
            if_modified_since: ims ?
                (new Date(ims)).getTime() : null,
            no_cache: !_.isUndefined(ua_cc['no-cache']),
            max_age: _.isUndefined(ua_cc['max-age']) ?
                $this.options.max_age : ua_cc['max-age']
        };

        $this.get(cache_key, opts, function (err, headers, body, meta) {
            if (err == $this.ERR_MISS || err == $this.ERR_STALE) {
                return $this.revalidate(cache_key, opts, req, res, err, response_cb);
            } else if (err == $this.ERR_NOT_MODIFIED) {
                return $this.sendNotModified(req, res, meta);
            } else {
                return $this.sendCacheEntry(cache_key, req, res, headers, body, meta);
            }
        });
    },

    // ### revalidate
    //
    // Runs the real response handler, caches the result if necessary.
    //
    // Lots of monkeypatching here to intercept the response events and build
    // the cache entry for storage. 
    //
    revalidate: function (cache_key, opts, req, res, err, response_cb) {
        var $this = this,
            cached_headers = [],
            cached_meta = { },
            cached_body_chunks = [],
            status_code = 999;

        // If there's no cache_key, skip all the caching monkeybusiness,
        if (!cache_key) {
            return response_cb(req, res);
        }

        // Monkeypatch to capture response headers
        var orig_setHeader = res.setHeader;
        res.setHeader = function (name, value) {
            orig_setHeader.apply(this, arguments);
            cached_headers.push([name, value]);

            name_lc = name.toLowerCase();
            if ('etag' == name_lc) {
                cached_meta.etag = value;
            }
            if ('last-modified' == name_lc) {
                cached_meta.last_modified = (new Date(value)).getTime();
            }
        };

        // Monkeypatch to set a Last-Modified header, if none in response
        var orig_writeHead = res.writeHead;
        res.writeHead = function (status, headers) {
            status_code = status;
            if (200 == status && _.isUndefined(cached_meta.last_modified)) {
                res.setHeader('Last-Modified', (new Date()).toUTCString());
            }
            orig_writeHead.apply(this, arguments);
        };

        // Cache body chunks as they come in.
        var cache_chunk = function (chunk, encoding) {
            // TODO: This *could* be trouble, if the response encoding isn't
            // UTF-8. The cache will have it encoded as UTF-8, but headers will
            // say otherwise. Probably good for now, though, since we're using
            // UTF-8 end-to-end in kumascript.
            cached_body_chunks.push(new Buffer(chunk, encoding)
                .toString('utf-8'));
        };

        // Monkeypatch to capture written body chunks
        var orig_write = res.write;
        res.write = function (chunk, encoding) {
            orig_write.apply(this, arguments);
            cache_chunk(chunk, encoding);
        };

        // Monkeypatch to capture the final send and cache the response
        var orig_send = res.send;
        res.send = function (chunk, encoding) {
            orig_send.apply(this, arguments);
            if (200 == status_code) {
                // Catch the last chunk, if any.
                if (chunk) { cache_chunk(chunk, encoding); }
                var cached_body = cached_body_chunks.join('');
                $this.set(cache_key, opts.max_age, cached_headers, cached_body, cached_meta,
                    function (err, headers, body, meta) {
                        /* No-op, fire and forget. */
                    }
                );
            }
        };

        // TODO: addTrailers?

        // Finally, signal to the origin handler that it's time to revalidate.
        return response_cb(req, res);
    },

    // ### sendCacheEntry
    //
    // Send the content from the cached response entry
    //
    sendCacheEntry: function (cache_key, req, res, headers, body, meta) {
        var $this = this;

        $this.sendCommonHeaders(req, res, meta);

        res.header('X-Cache', 'HIT');
        
        _.each(headers, function (header) {
            res.header(header[0], header[1]);
        });
        
        var method = req.method.toUpperCase();
        if ('HEAD' != method) {
            res.write(body, 'utf-8');
        }
        
        res.end();
    },

    // ### sendNotModified
    //
    // When a conditional GET signals no need to send content, this handles
    // sending the 304 Not Modified.
    //
    sendNotModified: function (req, res, meta) {
        this.sendCommonHeaders(req, res, meta);
        return res.send(304);
    },

    // ### sendCommonHeaders
    //
    // Send headers common to many responses.
    //
    sendCommonHeaders: function (req, res, meta) {
        var $this = this,
            now = (new Date());

        res.header('Age', Math.floor((now - meta.last_modified) / 1000));
        res.header('Date', new Date().toUTCString());
        if (meta.last_modified) {
            res.header('Last-Modified', new Date(meta.last_modified).toUTCString());
        }
        if (meta.etag) {
            res.header('ETag', meta.etag);
        }
    },

    // ### set
    //
    // Store a cache entry
    //
    set: function (key, max_age, headers, body, meta, next) {
        var $this = this;
        meta = meta || {};
        max_age = max_age || this.options.max_age;
        $this.memcached.set(key + ':headers', headers, max_age, function (e, r) {
            $this.memcached.set(key + ':body', body, max_age, function (e, r) {
                $this.memcached.set(key + ':meta', meta, max_age, function (e, r) {
                    next(null, headers, body, meta);
                });
            });
        });
    },

    // ### get
    //
    // Get a cache entry
    //
    // Implements HTTP caching semantics and responds with content as well
    // as STALE, NOT_MODIFIED, MISS errors
    //
    get: function (key, options, next) {
        options = options || {};
        var $this = this;

        if (0 === options.max_age) {
            // If I really wanted to, a $this._hasMeta(key) would let me report
            // ERR_STALE instead of ERR_MISS. But, I wanted to skip hitting the
            // cache backend altogether (eg. memcache or filesystem)
            return next($this.ERR_MISS);
        }

        // See above.
        if (options.no_cache) { return next($this.ERR_MISS); }
        
        $this.memcached.get(key + ':meta', function (err, meta) {
            // Punt, if $this key is not even in the cache
            if (!meta) { return next($this.ERR_MISS); }

            // Check if the cache is too old
            if (typeof(options.max_age) != 'undefined') {
                var now = (new Date()).getTime(),
                    last_modified = meta.last_modified,
                    age = (now - last_modified) / 1000;
                if (age > options.max_age) {
                    return next($this.ERR_STALE, null, null, meta);
                }
            }

            // Check modified since, if necessary
            if (typeof(options.if_modified_since) != 'undefined' &&
                    (meta.last_modified <= options.if_modified_since)) {
                return next($this.ERR_NOT_MODIFIED, null, null, meta);
            }

            // Check the content etag, if necessary
            if (typeof(options.if_none_match) != 'undefined' &&
                    (options.if_none_match == meta.etag)) {
                return next($this.ERR_NOT_MODIFIED, null, null, meta);
            }

            $this.memcached.get(key + ':headers', function (err, headers) {
                $this.memcached.get(key + ':body', function (err, body) {
                    next(err, headers, body, meta);
                });
            });
        });
    }

});
Example #2
0
var Server = ks_utils.Class({

    // Service accepts these options:
    default_options: {
        // Boolean switch to control logging.
        logging: true,
        // Port on which the HTTP service will listen.
        port: 9000,
        // Template used to resolve incoming URL paths to document source URLs.
        document_url_template: "http://localhost:9001/docs/{path}?raw=1",
        // Template used to resolve macro names to URLs for the loader.
        template_url_template: "http://localhost:9001/templates/{name}?raw=1",
        // Root dir (relative) from which to load macros for the loader.
        template_root_dir: "macros",
        // Default cache-control header to send with HTTP loader
        template_cache_control: "max-age=3600",
        // Prefix used in headers intended for env variables
        env_header_prefix: 'x-kumascript-env-',
        // Max number of loader retries.
        loader_max_retries: 5,
        // Time to wait between loader retries.
        loader_retry_wait: 100,
        // Max number of macro workers
        numWorkers: 16,
        // Max number of concurrent macro processing jobs per request.
        workerConcurrency: 4,
        // Max number of jobs handled by a worker before exit
        workerMaxJobs: 64,
        // Number of milliseconds to wait before considering a macro timed out
        workerTimeout: 1000 * 60,
        // Number of times to retry a macro
        workerRetries: 3
    },

    // Build the service, but do not listen yet.
    initialize: function (options) {
        var $this = this,
            app = this.app = express();

        this.req_cnt = 0;
        this.statsd = ks_utils.getStatsD(this.options);

        // Build the root URL of the document API service.
        this.doc_base_url = ks_utils.get_base_url(
            this.options.document_url_template
        );

        if (this.options.memcache) {
            var mo = this.options.memcache;
            this.memcached = new Memcached(mo.server, mo.options || {});
        } else {
            // If the configuration is missing, use the fake stub cache
            this.memcached = new ks_utils.FakeMemcached();
        }

        if (this.options.macro_processor) {
            this.macro_processor = this.options.macro_processor;
        } else {
            var mp_options = _.extend(_.clone(this.options), {
                autorequire: this.options.autorequire,
                memcache: this.options.memcache,
                statsd: this.options.statsd,
                loader: {
                    module: __dirname + '/loaders',
                    class_name: 'FileLoader',
                    options: {
                        memcache: this.options.memcache,
                        statsd_conf: this.options.statsd_conf,
                        url_template: this.options.template_url_template,
                        cache_control: this.options.template_cache_control,
                        root_dir: this.options.template_root_dir,
                        max_retries: this.options.loader_max_retries,
                        retry_wait: this.options.loader_retry_wait
                    }
                }
            });
            this.macro_processor = new ks_macros.MacroProcessor(mp_options);
        }

        if (!this.macro_processor.options.doc_base_url) {
            this.macro_processor.options.doc_base_url = this.doc_base_url;
        }

        $this.macro_processor.startup(function () {
            // Configure the HTTP server...
            if ($this.options.logging) {
                // Configure a logger that pipes to the winston logger.
                var logger = morgan(
                    ':remote-addr - - [:date] ":method :url ' +
                    'HTTP/:http-version" :status :res[content-length] ' +
                    '":referrer" ":user-agent" :response-time',
                    {
                        stream: {
                            write: function(s) {
                                log.info(s.trim(), {
                                    source: "server",
                                    pid: process.pid
                                });
                            }
                        }
                    }
                );
                app.use(logger);
            }
            app.use(firelogger());
            // Set up HTTP routing, pretty simple so far...
            app.get('/', _.bind($this.root_GET, $this));
            app.get('/docs/*', _.bind($this.docs_GET, $this));
            app.post('/docs/', _.bind($this.docs_POST, $this));
            app.get('/macros/?', _.bind($this.macros_list_GET, $this));
            app.get('/healthz/?', _.bind($this.liveness_GET, $this));
            app.get('/readiness/?', _.bind($this.readiness_GET, $this));
            app.get('/revision/?', _.bind($this.revision_GET, $this));
        });
    },

    // Start the service listening
    listen: function (port) {
        port = port || this.options.port;
        this.server = this.app.listen(port);
    },

    // Close down the service
    close: function () {
        var $this = this;
        $this.macro_processor.shutdown(function () {
            if ($this.server) {
                $this.server.close();
            }
        });
    },

    // #### GET /
    //
    // Return something
    root_GET: function (req, res) {
        res.send('<html><body><p>Hello from KumaScript!</p></body></html>');
    },

    // #### GET /docs/*
    //
    // Process source documents, respond with the result of macro evaluation.
    docs_GET: function (req, res) {
        var $this = this,
            opts = {};

        // Vary caching on values of env vars, as well as X-FireLogger
        var pfx = this.options.env_header_prefix;
        var vary = _.chain(req.headers).keys().filter(function (key) {
            return 0 === key.indexOf(pfx);
        }).value();
        vary.push('X-FireLogger');
        res.set('Vary', vary.join(','));

        // Create a response cache instance
        var cache = new ks_caching.ResponseCache({
            memcache: this.options.memcache,
            statsd: $this.statsd,
        });
        cache.cacheResponse(req, res, opts, function (req, res) {
            var path = req.params[0],
                url_tmpl = $this.options.document_url_template,
                doc_url = ks_utils.tmpl(url_tmpl, {path: encodeURI(path)});

            var req_opts = {
                memcached: $this.memcached,
                statsd: $this.statsd,
                timeout: $this.options.cache_timeout || 3600,
                cache_control: req.get('cache-control'),
                url: doc_url,
            };
            ks_caching.request(req_opts, function (err, resp, src) {
                if (err) {
                    res.log.error('Problem fetching source document: ' + err.message, {
                        name: 'kumascript',
                        template: '%s: %s',
                        args: [err.name, err.message, err.options]
                    });
                    res.send('');
                } else {
                    $this._evalMacros(src, req, res);
                }
            });
        });
    },

    // #### POST /docs/
    //
    // Process POST body, respond with result of macro evaluation
    docs_POST: function (req, res) {
        var $this = this,
            buf = '';

        // TODO: Be more flexible with encodings.
        req.setEncoding('utf8');
        req.on('data', function (chunk) { buf += chunk; });
        req.on('end', function () {
            try {
                var src = buf.length ? buf : '';
                $this._evalMacros(src, req, res);
            } catch (err){
                // TODO: Handle errors more gracefully
                $this._evalMacros('', req, res);
            }
        });
    },

    // #### GET /macros
    //
    // Get JSON of available macros (also known as templates)
    macros_list_GET: function (req, res) {
        var loader = this.macro_processor.makeLoader(),
            loader_name = this.macro_processor.options.loader.class_name,
            data = loader.macros_data();
        data.loader = loader_name;
        res.json(data);
    },

    /**
     * A "liveness" endpoint for use by Kubernetes or other
     * similar systems. A successful response from this endpoint
     * simply proves that this Express app is up and running. It
     * doesn't mean that its supporting services (like the macro
     * loader and the document service) can be successfully used
     * from this service.
     */
    liveness_GET: function (req, res) {
        res.sendStatus(204);
    },

    /**
     * A "readiness" endpoint for use by Kubernetes or other
     * similar systems. A successful response from this endpoint goes
     * a step further and means not only that this Express app is up
     * and running, but also that one or more macros have been found
     * and that the document service is ready.
     */
    readiness_GET: function (req, res) {
        var msg = 'service unavailable ',
            kumaReadiness = url.resolve(this.doc_base_url, 'readiness');

        // First, check that we can load some macros.
        try {
            // If there are no macros or duplicate macros, an error
            // will be thrown.
            this.macro_processor.makeLoader();
        } catch(err) {
            msg += `(macro loader error) (${err})`;
            res.status(503).send(msg);
            return;
        }

        // Finally, check that the document service is ready.
        request.get(kumaReadiness, function (err, resp, body) {
            if (!err && ((resp.statusCode >= 200) && (resp.statusCode < 400))) {
                res.sendStatus(204);
            } else {
                var reason = err ? err : body;
                msg += `(document service is not ready) (${reason})`;
                res.status(503).send(msg);
            }
        });
    },

    // #### GET /revision
    //
    // Return the value of the git commit hash for HEAD.
    revision_GET: function (req, res) {
        res.set({'content-type': 'text/plain; charset=utf-8'})
           .send(process.env.REVISION_HASH || "undefined");
    },

    // #### _evalMacros()
    //
    // Shared macro evaluator used by GET and POST
    _evalMacros: function (src, req, res) {
        try {
            // Extract env vars from request headers
            var pfx = this.options.env_header_prefix;
            var env = _.chain(req.headers).map(function (val, key) {
                try {
                    if (0 !== key.indexOf(pfx)) { return; }
                    var d_key = key.substr(pfx.length),
                        d_json = (new Buffer(val, 'base64'))
                                  .toString('utf-8'),
                        data = JSON.parse(d_json);
                    return [d_key, data];
                } catch (e) {
                    // No-op, ignore parsing errors for env vars
                }
            }).object().value();

            var cc = req.get('cache-control') || '';
            env.cache_control = cc;
            if (cc.indexOf('no-cache') != -1) {
                env.revalidate_at = (new Date()).getTime();
            }

            // Add a clone of the interactive-examples settings to "env".
            env.interactive_examples = ks_conf.nconf.get(
                'interactive_examples'
            );

            var ctx = {
                env: env,
                log: res.log
            };

            // Process the macros...
            this.macro_processor.process(src, ctx, function (errors, result) {
                if (errors) {
                    errors.forEach(function (error) {
                        delete error.options.src;
                        res.log.error(error.message, {
                            name: 'kumascript',
                            template: '%s: %s',
                            args: [error.name, error.message, error.options]
                        });
                    });
                }
                res.send(result);
            });

        } catch (error) {
            res.log.error(error.message, {
                name: 'kumascript',
                template: '%s: %s',
                args: [error.name, error.message, error.options]
            });
            // HACK: If all else fails, send back the source
            res.send(src);
        }
    }

});
Example #3
0
var List = module.exports = Class({
    // コンストラクタ
    initialize : function(){
        this.list_ = new LinkedList.Manager();
    },
    // 後始末
    finalize : function(){
        this.list_ = null;
    },
    push : function(o){
        var n = new LinkedList.Node();
        n.data = o;
        this.list_.pushTail(n);
    },
    unshift : function(o){
        var n = new LinkedList.Node();
        n.data = o;
        this.list_.pushHead(n);
    },
    pop : function(){
        var data = null;
        if(this.list_.size() > 0){
            var n = this.list_.popTail();
            data = n.data;
            n.data = null;
        }
        return data;
    },
    shift : function(){
        var data = null;
        if(this.list_.size() > 0){
            var n = this.list_.popHead();
            data = n.data;
            n.data = null;
        }
        return data;
    },
    // キューをクリアする
    clear : function(){
        this.list_.finalize();
    },
    // キューの長さを取得する
    size : function(){
        return this.list_.size();
    },
    // キューをスキャンする
    scan : function(callback){
        var i = 0;
        this.list_.scanHead(function(node){
            if(!callback(i++,node.data)){
                return false;
            }
            return true;
        });
        return true;
    },
});
Example #4
0
var MacroProcessor = ks_utils.Class(EventEmitter, {

    // #### Default options
    default_options: {
        loader: {
            module: __dirname + '/loaders',
            class_name: 'HTTPLoader',
            options: {
                filename_template: __dirname + '/fixtures/templates/{name}.ejs'
            }
        },
        memcache: null,
        // Max number of macro workers
        numWorkers: 16,
        // Max number of concurrent macro processing jobs per request.
        workerConcurrency: 4,
        // Max number of jobs handled by a worker before exit
        workerMaxJobs: 64,
        // Number of milliseconds to wait before considering a macro timed out
        workerTimeout: 1000 * 60 * 10,
        // Number of times to retry a failed macro
        workerRetries: 3
    },

    initialize: function (options) {
        this.statsd = ks_utils.getStatsD(this.options);
    },

    startup: function (next) {
        var $this = this;

        var pool = this.worker_pool = new hirelings.Pool({
            module: __dirname + '/macros-worker.js',
            max_processes: this.options.numWorkers,
            max_jobs_per_process: this.options.workerMaxJobs,
            retries: this.options.workerRetries,
            options: {
                autorequire: this.options.autorequire,
                loader: this.options.loader,
                memcache: this.options.memcache
            }
        });
        
        // HACK: Maybe hirelings should support StatsD directly
        ['spawn', 'exit', 'task', 'backlog', 'drain']
            .forEach(function (name) {
                pool.on(name, function () {
                    $this.statsd.increment('workers.pool.' + name);
                    $this.measureWorkers();
                });
            });

        return next();
    },

    shutdown: function (next) {
        if (this.worker_pool) { this.worker_pool.exit(); }
        return next();
    },

    // #### Process macros in content
    process: function (src, ctx, process_done) {
        var $this = this;
        var errors = [];
        var macros = {};

        // Common exit point for processing
        var t_process_start = t_now();
        function _done(errors, src) {
            $this.statsd.timing('macros.t_processing',
                    t_now() - t_process_start);
            if (!errors.length) errors = null;
            return process_done(errors, src);
        }

        // Attempt to parse the document, trap errors
        var tokens = [];
        try { tokens = ks_parser.parse(src); }
        catch (e) {
            errors.push(new ks_errors.DocumentParsingError({
                error: e, src: src
            }));
            return _done(errors, src);
        }

        var autorequire = $this.options.autorequire;
        var templates_to_reload = [];
        if (ctx.env && 'no-cache' == ctx.env.cache_control) {
            // Extract a unique list of template names used in macros.
            var template_names = _.chain(tokens)
                .filter(function (tok) { return 'MACRO' == tok.type; })
                .map(function (tok) { return tok.name; })
                .uniq().value();
                
            // Templates to flush include those used in macros and
            // autorequired modules
            templates_to_reload = _.union(template_names, _.values(autorequire)); 
        }

        // Intercept the logger from context, if present.
        var log = null;
        if ('log' in ctx) {
            log = ctx.log;
        }

        // Macro processing queue managing process of sending jobs to the
        // evaluation cluster.
        //
        // Yes, this is an (internal) queue managing submissions to another
        // (external) queue. But, it limits the number of concurrent jobs per
        // document, and tells us when this document's macros are done.
        var macro_q = async.queue(function (hash, q_next) {

            var t_enqueued = t_now();
            var t_started = null;

            var token = macros[hash];
            var work = {token: token, src: src, ctx: ctx};

            var job = $this.worker_pool.enqueue(work, function (err, rv) {

                if (err) {
                    errors.push(new ks_errors.TemplateExecutionError({
                        error: err,
                        stack: '',
                        name: token.name,
                        src: src,
                        token: token
                    }));
                    token.out = '{{ ' + token.name + ' }}';
                } else if (rv.error) {
                    var err_cls = rv.error[0];
                    var err_opts = rv.error[1];
                    errors.push(new ks_errors[err_cls](err_opts));
                    token.out = '{{ ' + token.name + ' }}';
                } else {
                    token.out = rv.result;
                }
                
                var t_running = t_now() - t_started;
                
                $this.statsd.timing('macros.t_running.overall', t_running);
                $this.statsd.timing('macros.t_running.by_name.' + token.name,
                                    t_running);
                $this.statsd.timing('macros.t_enqueued', t_now() - t_enqueued);
                $this.statsd.increment('macros.by_name.' + token.name);

                q_next();
            });

            job.on('start', function () {
                t_started = t_now();
            });

            job.on('progress', function (msg) {
                var type = msg.type,
                    args = msg.args;
                switch (type) {
                    case "log":
                        log[args[0]](args[1]);
                        break;
                    case "statsd":
                        $this.statsd[args[0]](args[1]);
                        break;
                }
            });

            job.on('retry', function () {
                $this.statsd.increment('macros.retries.overall');
                $this.statsd.increment('macros.retries.' + token.name);
            });

        }, $this.options.workerConcurrency);

        // Before queueing macros for processing, reload templates (if any)
        $this.reloadTemplates(templates_to_reload, function (err) {
            
            // Scan through the tokens, queue unique macros for evaluation.
            tokens.forEach(function (token) {
                if ('MACRO' == token.type) {
                    token.hash = $this.hashTokenArgs(token);
                    if (!(token.hash in macros)) {
                        macros[token.hash] = token;
                        macro_q.push(token.hash);
                    }
                }
            });

            // Exit point when the processing queue has drained.
            macro_q.drain = function (err) {
                // Assemble output text by gluing together text tokens and the
                // results of macro evaluation.
                var src_out = _.map(tokens, function (token) {
                    if ('TEXT' == token.type) {
                        return token.chars;
                    } else if ('MACRO' == token.type) {
                        return macros[token.hash].out;
                    }
                }).join('');
                return _done(errors, src_out);
            }

            // If no macros were queued up, jump straight to drain.
            if (0 == macro_q.length()) { macro_q.drain(); }

        });
    },

    // #### Produce a unique hash for macro
    // A macro's unique hash encompasses the template name and the arguments
    hashTokenArgs: function (token) {
        // Hash the macro name and args, to identify unique calls.
        var hash = crypto.createHash('md5').update(token.name);
        if (token.args.length > 0) {
            // Update the hash with arguments, if any...
            if (_.isObject(token.args[0])) {
                // JSON-style args, so stringify the object.
                hash.update(JSON.stringify(token.args));
            } else {
                // Otherwise, this is a simple string list.
                hash.update(token.args.join(','));
            }
        }
        return hash.digest('hex');
    },

    // #### Force-reload the named templates, if any.
    reloadTemplates: function (names, done) {
        if (0 == names.length) { return done(); }
        try {
            var loader_module = require(this.options.loader.module);
            var loader_class = loader_module[this.options.loader.class_name];
            var loader_options = _.clone(this.options.loader.options);

            // Use a Cache-Control header that forces a fresh cache.
            loader_options.cache_control = 'no-cache';
            loader_options.statsd = this.statsd;

            var loader = new loader_class(loader_options);
            async.forEach(names, function (name, e_next) {
                loader.get(name, e_next);
            }, done);
        } catch (e) {
            done(e);
        }
    },

    // #### Record some measurements from the worker pool.
    measureWorkers: function () {
        var stats = this.worker_pool.getStats();
        this.statsd.gauge('workers.total', stats.workers);
        this.statsd.gauge('workers.busy', stats.busy);
        this.statsd.gauge('workers.backlog', stats.backlog);
    }

});
Example #5
0
    fibers = require('fibers'),
    Future = require('fibers/future'),
    wait = Future.wait,
    ks_utils = require(__dirname + '/utils');

// ### BaseTemplate
//
// The base template script class
var BaseTemplate = ks_utils.Class({

    default_options: {
        // Templates compile from textual source
        source: ''
    },

    // #### execute
    //
    // Execute the template with the given arguments. The callback should expect
    // `(err, result)` parameters.
    execute: function (args, ctx, next) {
        next(null, "UNIMPLEMENTED");
    }

});

// ### JSTemplate
//
// Template executed using sandboxed JS
var JSTemplate = ks_utils.Class(BaseTemplate, {
    initialize: function (options) {
        BaseTemplate.prototype.initialize.apply(this, arguments);
        var vm = require("vm");
Example #6
0
var Job = Class({
    // コンストラクタ
    initialize : function(
        interval,   // 実行間隔
        next,       // 処理開始時間
        callback    // 処理関数
    ){
        this.interval_ = interval;
        this.next_ = next;
        this.callback_ = callback;
    },
    // 後始末
    finalize : function(){
        this.interval_ = 0;
        this.next_ = 0;
        this.callback_ = null;
    },
    // 次の仕事をスケジューリングする
    next : function(){
        this.next_ += this.interval_;
    },
    // スケジューリングされている時間を経過したかチェックする
    isExec : function(now){
        if(now >= this.next_){
            return true;
        }
        return false;
    },
    // 処理を実行する
    exec : function(now){
        if(this.callback_){
            this.callback_(now);
        }
    },
});
Example #7
0
var HashMap = module.exports = Class({
    /**
     * コンストラクタ
     */
    initialize : function(){
        this.hash_ = {};
        this.size_ = 0;
    },
    /**
     * サイズの取得
     * @returns {Number} 値
     */
    size : function(){
        return this.size_;
    },
    /**
     * キーから値を取得
     * @param {String} key キー
     * @returns {Valiant} 値
     */
    get : function(key){
        assert(this.isExist(key));
        return this.hash_[key];
    },
    /**
     * キーで値を設定
     * @param {String} key キー
     * @param {Valiant} value 値
     */
    set : function(key,value){
        if(this.hash_[key] === undefined){
            ++this.size_;
        }
        this.hash_[key] = value;
    },
    /**
     *  キーが存在するかどうか
     * @param {String} key キー
     * @returns {Boolean} 存在可否
     */
    isExist : function(key){
        return (this.hash_[key] !== undefined);
    },
    /**
     * キーを削除
     * @param {String} key キー
     */
    remove : function(key){
        assert(this.isExist(key));
        --this.size_;
        assert(this.size_ >= 0);
        delete this.hash_[key];
    },
    /**
     * スキャン関数
     * 処理関数にfalseを返させると途中で処理を終了する
     * @param {Function} callback 処理関数function(key,value){return true;}
     */
    scan : function(callback){
        var w = this.hash_;
        for(var key in w) if(w.hasOwnProperty(key)){
            if(!callback(key, w[key])){
                return false;
            }
        }
        return true;
    },
});