経済産業省が先日リリースした「gBizINFO IMI 情報共有基盤 住所変換コンポーネント」を使っているが、このプログラムを実用する上に当たって問題がある。
この住所変換コンポーネントは並列で使えない
下のプログラムはコンポーネントの main.js
を引用している。
見ていただきたいのは、 export されている関数はひとつだけであり、この中で LevelDB の初期化とクローズを行っていることだ。このため、住所を変換する関数を呼ぶたびに、DBのインスタンスが生成・破棄されることになる。
const fs = require("fs");
const find = require("./lib/find");
const bangou = require("./lib/bangou");
const util = require("./lib/util");
const levelup = require('levelup');
const leveldown = require('leveldown');
// 住所から緯度経度付き場所型を返す
module.exports = function(src) {
const db = levelup(leveldown(__dirname + "/db")); // ←著者注: DB初期化
const promises = targets.map(target => {
// ...
return db.get(code, { // ←著者注: DB読む
asBuffer: false
}).then(str => {
// ...
}).catch(e => {
// ...
});
});
return Promise.all(promises).then(() => db.close()).then(() => dst); // ←著者注: DBインスタンス破棄
};
そのため、例えば、多数の行がある CSV の住所を取り出して全てのカラムを変換したいときに、こういうプログラムを書くと、
// ...
const transform = csv.transform(async (data, cb) => {
const normalizedAddress = await address(data[2]); // ← これが非同期的に何度も呼ばれる
cb(null, [..., someFunction(normalizedAddress), ...])
});
// ...
inputStream.pipe(parse)
.pipe(transform)
.pipe(stringify)
.pipe(outStream)
}())
このようにロックのエラーが出る。これは非同期として処理しているため、並列で LevelDB のデータベースを開こうとしているために起きる。
OpenError: IO error: lock xxx/address-normalizer/node_modules/imi-enrichment-address/db/LOCK: already held by process
at xxx/address-normalizer/node_modules/levelup/lib/levelup.js:119:23
at xxx/address-normalizer/node_modules/abstract-leveldown/abstract-leveldown.js:38:14
at xxx/address-normalizer/node_modules/deferred-leveldown/deferred-leveldown.js:31:21
at xxx/address-normalizer/node_modules/abstract-leveldown/abstract-leveldown.js:38:14
クラス化して解決する
このため、クラスにすることで解決する。
クラスのコンストラクタでDBを初期化して、開きっぱなしにすることで、非同期的に呼んでもロックの問題は起きない。
また、この形式にすると、繰り返し呼ぶプログラムでは高速化されるはずだ。
このコンポーネントは GitHub 上になく、かつライセンスがどこにも記載されていないため、こちらに簡単な解決法を著作権法上認められた最低限の引用の形で記しておく:
なおこのパッチは住所変換コンポーネント v.2.0.0 のみ対応である。
--- main.org.js 2020-06-08 12:11:19.000000000 +0900
+++ main.js 2020-06-08 12:19:19.000000000 +0900
@@ -8,7 +8,16 @@
const leveldown = require('leveldown');
// 住所から緯度経度付き場所型を返す
-module.exports = function(src) {
+class EnrichmentAddress {
+ constructor() {
+ this.db = levelup(leveldown(__dirname + "/db"));
+ }
+
+ disconnect() {
+ this.db.close();
+ }
+
+ convert(src) {
const dst = typeof src === 'string' ? {
"@context": "https://imi.go.jp/ns/core/context.jsonld",
@@ -40,7 +49,6 @@
return Promise.resolve(dst);
}
- const db = levelup(leveldown(__dirname + "/db"));
const promises = targets.map(target => {
const address = target["住所"] || target;
const response = find(address["表記"]);
@@ -67,7 +75,7 @@
code = code + t;
}
- return db.get(code, {
+ return this.db.get(code, {
asBuffer: false
}).then(str => {
let json = JSON.parse(str);
@@ -155,7 +163,9 @@
return true;
});
});
- return Promise.all(promises).then(() => db.close()).then(() => dst);
+ return Promise.all(promises).then(() => dst);
+}
+}
+module.exports.EnrichmentAddress = EnrichmentAddress;
-};
patch
コマンドを使って、 node_modules/imi-enrichment-address/main.js
にパッチを当てて上書きすると良いだろう。
Mac
上の diff をコピーして、(最後の行まで)
pbpaste | patch node_modules/imi-enrichment-address/main.js
Linux
上の diff ファイルを main.js.diff
という名前で保存して、(最後の行まで)
patch node_modules/imi-enrichment-address/main.js < main.js.diff
クラス化バージョンの使い方
const enrichAddr = new EnrichmentAddress();
console.log(await enrichAddr.convert("住所を入れます"))
// 処理が終わった後にDBをクローズすること
// 下記は例えばストリームの処理が終わった後にクローズする例
xxx.on('end', () => enrichAddr.disconnect())