LoginSignup
7
5

More than 5 years have passed since last update.

同時アクセス時に重いタスクを1度だけ実行する

Posted at

特定のURLへアクセスしたさい、コマンドラインの実行や、ファイルのコンパイルを行い、成果物を返すようなAPIを作成したい場合があります。

たとえば、以下の例では、/articles/にアクセスすると、index.mdindex.htmlにコンパイルした結果を返します。

pct/
git clone https://github.com/59798/promise-cache-task.git pct
cd pct
git checkout e0922b6b4e3caf8f8610ef3e074eb37ce3314072

npm install
pct/src/index.js
// Dependencies
import Bluebird from 'bluebird';

import { Router as expressRouter } from 'express';
import fsOrigin from 'fs';
import marked from 'marked';

// Module enhancement
const fs = Bluebird.promisifyAll(fsOrigin);
const markedAsync = Bluebird.promisify(marked);

// Public
export default (cwd) => {
  const router = expressRouter();

  router.use((req, res, next) => {
    let filePath = req.url.slice(1);
    if (filePath === '' || filePath.match(/\/$/)) {
      filePath += 'index';
    }
    const fileName = `${cwd}/${filePath}.md`;
    const cacheName = `${cwd}/${filePath}.html`;

    const notFound = fs.existsSync(fileName) === false;
    const useCache = fs.existsSync(cacheName);
    if (notFound) {
      return next();
    }
    if (useCache) {
      return res.sendFile(cacheName);
    }

    console.log('以降の処理は重いので1度だけ実行したい');

    return fs.readFileAsync(fileName)
    .then((data) => markedAsync(data.toString()))
    .then((cache) => {
      const trimedCache = cache.trim();// 末尾"\n"の削除

      return fs.writeFileAsync(cacheName, trimedCache)
      .then(() => {
        res.set('content-type', 'text/html');
        res.end(trimedCache);
      });
    });
  });

  return router;
};
test/index.js
// Dependencies
import middleware from '../src';

import express from 'express';
import caravan from 'caravan';
import assert from 'power-assert';
import del from 'del';

// Environment
const cwd = `${__dirname}/fixtures/public`;
const port = process.env.PORT || 59798;

// Specs
describe('markedown compile server', () => {
  let server;
  before((done) => {
    const app = express();
    app.use('/articles', middleware(cwd));

    server = app.listen(port, done);
  });
  after((done) => (
    server.close(() => {
      del(`${cwd}/**/*.html`).then(() => done());
    })
  ));

  it('markdownをhtmlとしてコンパイル返す。htmlはキャッシュとして保存し、以降はコンパイルしない', () => {
    const concurrency = 100;
    const urls = [];
    for (let i = 0; i < concurrency; i++) {
      urls.push(`http://localhost:${port}/articles`);
    }

    return caravan(urls, { concurrency })
    .progress((progress) => {
      assert.equal(progress.value, '<p><strong>要反省である</strong></p>');
    })
    .then((responses) => {
      assert.equal(responses.length, concurrency);
    });
  });
});

上記コードは、コンパイル後のindex.htmlを利用することを想定していますが、テストでは100リクエストをほぼ同時に行うため、重いタスクを何度も実行してしまっていることが確認できます。

pct/
npm test
# markedown compile server
# 以降の処理は重いので1度だけ実行したい
# 以降の処理は重いので1度だけ実行したい
# 以降の処理は...
# ...
#   ✓ markdownをhtmlとしてコンパイル返す。htmlはキャッシュとして保存し、以降はコンパイルしない
# 1 passing (159ms)

そこで、タスクの進捗をPromiseに保存し、2度目以降の要求を保留にすることで、これを回避することができます。

pct/
git checkout master
npm test
# markedown compile server
# 以降の処理は重いので1度だけ実行したい
#   ✓ markdownをhtmlとしてコンパイル返す。htmlはキャッシュとして保存し、以降はコンパイルしない
# 1 passing (59ms)
7
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
5