Node.js でバックグラウンドジョブを自前実装する:PostgreSQL でジョブ管理

BullMQ や外部キューサービスを使わずに、PostgreSQL + while(true) ループでバックグラウンドジョブを管理する仕組みを作った。 「外部依存を増やしたくない」「DB を見るだけでジョブの状態がわかるようにしたい」という理由から。 ⚠️ この実装の前提条件(割り切りポイント) この仕組みは 「予算を抑えたい個人開発」かつ「アプリ単一インスタンス(1プロセス)」 での運用を前提に、あえてシンプルに作っています。 以下のトレードオフを理解した上で使ってください。 1. 厳密な重複排除はしていない(レースコンディション) アプリ側で isAnyRunning をチェック → ジョブ作成という 2 ステップになっているため、ミリ秒単位で同時リクエストが来た場合はすり抜ける可能性があります。 厳密に防ぐなら、後述する DB 側の Partial Unique Index が必要です。 2. マルチインスタンス非対応 起動時にジョブを一括リセットしているため、コンテナを複数台並列で動かす(水平拡張する)場合は、他インスタンスで実行中のジョブを巻き添えにします。 複数台にするなら worker_id カラムを導入するか、一括リセットをやめてください。 「バグ」ではなく「この規模だからこその意図的な割り切り」です。BullMQ を検討する規模になったら移行サインと考えています。 jobs テーブルの設計 CREATE TABLE jobs ( id SERIAL PRIMARY KEY, type VARCHAR(50) NOT NULL, status VARCHAR(20) NOT NULL DEFAULT 'pending', log TEXT, started_at TIMESTAMPTZ, finished_at TIMESTAMPTZ, created_at TIMESTAMPTZ DEFAULT NOW() ); status は pending → running → done / error と遷移する。 log カラムにジョブの実行ログを蓄積するので、Web UI から確認できる。 ...

May 24, 2026 · 2 min

ReactのuseStateでDate.now()を使うとlintエラーになる話

何が起きたか useState の初期値で Date.now() を直接呼んでいたら、こんなエラーが出た。 error Error: Cannot call impure function during render `Date.now` is an impure function. コード的にはこういうやつ。 const [stockPriceFrom, setStockPriceFrom] = useState<string>( new Date(Date.now() - 90 * 86400000).toISOString().split('T')[0] ) なぜエラーになるか React のルールとして、レンダー中はコンポーネントが pure でなければならない。 Date.now() や Math.random() はレンダーのたびに異なる値を返す「impure な関数」なので、直接渡すと React(特に Strict Mode)に怒られる。 Strict Mode ではレンダーを意図的に 2 回実行するため、こういった副作用が顕在化しやすい。 解決策:lazy initialization useState に関数を渡すと、初回マウント時に一度だけ実行される。これが lazy initialization パターン。 const [stockPriceFrom, setStockPriceFrom] = useState<string>( () => new Date(Date.now() - 90 * 86400000).toISOString().split('T')[0] ) const [stockPriceTo, setStockPriceTo] = useState<string>( () => new Date(Date.now() - 84 * 86400000).toISOString().split('T')[0] ) () => で包むだけ。それだけ。 まとめ パターン 評価タイミング useState(Date.now()) レンダーごとに評価される useState(() => Date.now()) 初回マウント時のみ new Date() も同様なので、日付系の初期値を useState に渡すときはアロー関数で包む癖をつけておくと良い。 ...

May 23, 2026 · 1 min

モダンオペレーティングシステム 第3章中盤メモ

モダンオペレーティングシステム 第5版 上 第3章中盤(3.3後半〜3.5)。ページテーブルの実装とTLB、ページ置き換えアルゴリズム。 3.3後半 ページテーブルの実装 多段ページテーブル 仮想アドレス空間が64bitの場合、単純なページテーブルは現実的じゃない。 理論上の最大:2^64 = 18,446,744,073,709,551,616 バイト 1ページ = 4KB = 4096バイト ページ数 = 2^64 / 2^12 = 2^52 個 1エントリ = 8バイトとして ページテーブルのサイズ = 2^52 × 8 = 32ペタバイト プロセス1個のページテーブルだけで32PB。話にならない。 実際のx86_64は64bitフルを使わず48bitに妥協している(一部の最新CPUは57bit)。64bitフルを使うと6〜7段の階層が必要になりメモリアクセスのオーバーヘッドが耐えられなくなるから。「理論上の最大は64bitだけど、現実のCPUは48bitに妥協している」という設計判断。 Linuxはこれを多段ページテーブルで解決してる。x86_64では4段構成。 仮想アドレス(48bit有効) ┌──────┬──────┬──────┬──────┬────────────┐ │ PGD │ PUD │ PMD │ PTE │ offset │ │ 9bit │ 9bit │ 9bit │ 9bit │ 12bit │ └──────┴──────┴──────┴──────┴────────────┘ 名前は覚えなくていい。住所の階層構造だと思えばいい。 東京都 → PGD(一番大きい区分) 渋谷区 → PUD 代々木1丁目 → PMD 1番地 → PTE 101号室 → offset(ページ内の位置) ポイントは使っていない部分のテーブルを作らないこと。 ...

May 10, 2026 · 4 min

モダンオペレーティングシステム 第3章後半メモ

モダンオペレーティングシステム 第5版 上 第3章後半(3.6〜3.8)。実装上の課題、セグメンテーション、そしてCPU脆弱性とOSの関係。 3.6 実装上の課題 OSがページングに関わるタイミング ページング処理はいつ動くか。 【プロセス生成時】 → 新しいページテーブルを作る → ディスクからプログラムをロードする準備 【プロセス実行時】 → TLBミス → ページテーブルをたどる → ページフォルト → ページを物理メモリに載せる 【プロセス終了時】 → ページテーブルを解放 → 物理フレームを解放 → ディスク上のスワップ領域を解放 【コンテキストスイッチ時】 → ページテーブルの切り替え → TLBフラッシュ or ASID切り替え k3sでPodが終了した時: コンテナプロセス終了 → カーネルがページテーブル解放 → 物理フレーム解放 → 次のPodがそのフレームを使える 命令の再実行問題 ページフォルトが起きた時、フォルトした命令を再実行する。でもここに厄介な問題がある。 メモリのコピー命令(100番地→200番地にコピー) 実行途中の150番地でページフォルト ↓ 100〜149はすでにコピー済み ↓ 命令を再実行 ↓ 100〜149を二重にコピー → 壊れる 対処法 方法1: 命令実行前に全ページが存在するか先読みチェック → なければ先にページフォルトを起こしておく → 全部揃ってから命令実行(x86寄りの方式) 方法2: CPUが内部状態を保存 → どこまで実行したか記録 → 再開時はその続きから(一部のRISCの方式) ページのロック(ピン留め) I/O処理中のページは絶対に追い出せない。 DMA転送中 → デバイスが直接メモリに書き込んでる → このページを追い出したら → デバイスが存在しないメモリに書き込む → 最悪システムクラッシュ だからI/O中のページには**ロック(ピン留め)**をかける。置き換えアルゴリズムの対象外になり、I/O完了後にロック解除。 ...

May 10, 2026 · 3 min

Raspberry Pi 2台構成のWiFi APでSSIDが起動時に出ない問題を解決した

構成 自宅NWの冗長化目的で Raspberry Pi を2台使い、WiFiアクセスポイントを組んでいる。 ホスト名 チャンネル SSID pi-1 1 your-ssid pi-2 11 your-ssid 同一SSIDで別チャンネルにする構成は ESS(Extended Service Set) と呼ばれ、クライアントが自動的に電波の強い方に接続する。WPA2-PSKで普通に成立する冗長化構成。 ブリッジ構成で bridge interface(br0)にWiFiインタフェース(wlan0)を参加させ、hostapdで電波を飛ばしている。 問題 OS再起動後、SSIDが片方または両方スキャンに出ない。 systemctl status hostapd は active (running) エラーログも一見出ていない hostapdを手動で再起動すると出る 調査 チャンネルの干渉を疑う 最初にhostapd.confのチャンネル設定を確認した。 2.4GHz帯で実際に干渉しないチャンネルは 1、6、11 の3つだけ。元の設定は6と7だったため、ほぼ完全に干渉していた。 ch6 --|-- ch7 --|-- ← 隣接していてほぼ被る チャンネルを1と11に変更した。 # pi-1 channel=1 # pi-2 channel=11 BSSIDレベルで電波を確認 sudo iw dev wlan0 scan | grep -E "BSS |SSID|freq|signal" BSS xx:xx:xx:xx:xx:xx(on wlan0) freq: 2462 signal: -15.00 dBm SSID: your-ssid BSS xx:xx:xx:xx:xx:xx(on wlan0) freq: 2412 signal: -68.00 dBm SSID: your-ssid 両台ともビーコンは出ていた。ESSとして動作は成立している。 ...

May 6, 2026 · 2 min

モダンオペレーティングシステム 第3章前半メモ

モダンオペレーティングシステム 第5版 上 第3章前半(3.1〜3.3)。メモリ管理の基本。 第3章前半 メモリ管理の基礎 ベースレジスタとリミットレジスタ CPUのハードウェアレジスタを使ったシンプルなメモリ保護の仕組み。 プログラムがメモリにロードされる ↓ ベースレジスタ ← プログラムの開始物理アドレス リミットレジスタ ← プログラムの長さ ↓ プロセスがメモリにアクセスするたびに ↓ CPUがアクセスアドレス + ベース値を自動加算 ↓ リミットを超えてないか同時チェック ↓ 超えてたらフォールト(アクセス中断) これによってプロセスAはプロセスBのメモリに触れない。CPUレベルで物理的にブロックしてる。 ProxmoxのVM(KVM)との絡み KVMの場合はベース/リミットだけじゃなく、**EPT(Extended Page Table)**というIntel VT-xの機能でさらに一段上の分離をしてる。 ゲストの仮想アドレス ↓ ゲストのページテーブル ↓ ゲストの物理アドレス(実はまだ仮想) ↓ EPT(ここがKVMの追加レイヤー) ↓ 本当の物理アドレス これをネストされたページングと呼ぶ。ゲストOSごとに完全に別の物理アドレス空間にマッピングされるので、VM同士はお互いのメモリに絶対アクセスできない。 EPTはソフトウェアじゃなくてCPUがハードウェアで変換するので速い。 KVMに必要なCPU機能 KVMは以下のCPU機能が必須: CPU 仮想化支援機能 Intel VT-x AMD AMD-V(SVM) ARM(ラズパイ4等) ARMv8 Virtualization Extensions BIOSでVT-xが無効になってるとKVMが動かない。ProxmoxでVM作れないエラーの原因の大半がこれ。 MacのDockerが遅かった理由もここ Intel Mac時代 → HyperKitでソフトウェア仮想化 → 遅い・重い Apple Silicon(M1〜) → ARMのVirtualization Extensions使える → ハードウェア仮想化 → 速い M1でDockerが速くなったのはApple SiliconがARMの仮想化支援機能をちゃんと使えるようになったから。ただしARMなのでx86イメージは--platform=linux/amd64でエミュレーションになり遅い。 ...

May 2, 2026 · 2 min

モダンオペレーティングシステム 第1章・第2章メモ

モダンオペレーティングシステム 第5版 上 読んでる。 読み進めた記事というよりは範囲の中でわからんとこを生成AIと相談しながら文章課題とか出してもらいつつ進めたものをまとめた記事。 第1章 序論 OSの2つの役割 OSには2つの顔がある。 1. 拡張マシン(Extended Machine) ハードウェアの複雑さを隠蔽する。アプリ開発者はディスクの物理構造を知らなくてもファイルを読み書きできる。Docker APIがLinuxの複雑さを隠すのと同じ発想。 2. リソースマネージャー(Resource Manager) CPU・メモリ・ディスク・ネットワークを複数プロセスに割り当てる。k3s schedulerがNodeのリソースをPodに割り当てるのと同じ役割。 k3s scheduler → OSのリソースマネージャーと同じ役割 Docker API → OSの拡張マシンと同じ役割 OSがやってることをk3sやDockerが上位レイヤーで再現している。 ユーザー空間とカーネル空間 ┌─────────────────────────────┐ │ ユーザー空間 │ │ Docker、アプリ、k3s、etc... │ │ → 直接ハードウェアは触れない │ └──────────┬──────────────────┘ │ システムコール(唯一の通路) ┌──────────▼──────────────────┐ │ カーネル空間 │ │ スケジューラ │ │ メモリ管理 │ │ ファイルシステム │ │ デバイスドライバ │ │ → ハードウェアを直接触れる │ └─────────────────────────────┘ アプリが直接ハードウェアを触れたら危険。カーネルが仲介することで安全性を保証する。 システムコール docker runした時の実際の流れ: docker run ubuntu ↓ Dockerデーモン ↓ clone() ← プロセス生成 unshare() ← namespaceを作る mount() ← ファイルシステムをマウント ↓ Linuxカーネル Dockerはシステムコールの集合体。魔法じゃなくてLinuxのシステムコールをうまく組み合わせてるだけ。 ...

April 26, 2026 · 3 min

Emacs 30.2 で *-ts-mode が壊れる問題と対処

結論 Mike Olson - Fixing typescript-ts-mode in Emacs 30.2 上記のブログで解説と対応スクリプトが公開されてる。 Emacs 30.2 + libtree-sitter 0.26 の組み合わせで go-ts-mode や typescript-ts-mode などの *-ts-mode が動作しない場合、現時点(2026年4月)では以下のワークアラウンドで動作させることができる。 現時点ではこれが最適解と思う。おそらくEmacs 31 安定版がリリースされるまでこのスクリプトを使い続けることになる。 mkdir -p ~/.emacs.d/init curl -o ~/.emacs.d/init/treesit-predicate-rewrite.el \ https://raw.githubusercontent.com/mwolson/emacs-shared/master/init/treesit-predicate-rewrite.el init.el の早い段階(他の *-ts-mode の設定より前)に追加する。 (load "~/.emacs.d/init/treesit-predicate-rewrite" nil nil nil t) 経緯 go-ts-mode を開いたとき *Messages* バッファに treesit-query-error が出てシンタックスハイライトが死んだ。 最初はグラマーの .so ファイルが原因だと思い、treesit-install-language-grammar で入れ直したり、~/.emacs.d/tree-sitter/ 以下のファイルを削除して再インストールしたりと試行錯誤した。しかし何をやっても症状が変わらず、グラマー側の問題ではないと判断した。 調べたところ Emacs bug#79687 に行き着いた。グラマーの問題ではなく、Emacs 30.2 と libtree-sitter 0.26 の組み合わせ自体が壊れていた。 症状 Emacs 30.2 で Go や TypeScript などのファイルを開くと、*Messages* バッファに以下のようなエラーが表示され、シンタックスハイライトが一切効かなくなる。 Error during redisplay: (jit-lock-function 35) signaled (treesit-query-error "Syntax error at" 73 "(call_expression function: ((identifier) @font-lock-builtin-face (#match \"...\" @font-lock-builtin-face)))" "Debug the query with `treesit-query-validate'") go-ts-mode だけでなく typescript-ts-mode、python-ts-mode、rust-ts-mode など、tree-sitter ベースの全モードで同様の問題が発生する。 ...

April 23, 2026 · 2 min

自宅NWをKeepalivedとdnsmasqで作り直した話

背景 自宅のネットワーク構成が辛かった。 具体的には以下の問題があった。 ルータが1台構成で、死んだら自宅NW全滅 メンテナンスで定期的に落としたいとかあっても、コストが大きい DNS/DHCPがルータに同居していて、役割が混在している WireGuardで外部接続していたが、外部からの入り方がだるい。 特に「冗長化したい」という気持ちは何年も前からあったが、DNSとDHCPの同期という難問の前に何度も挫折してきた。 結論からいえば、DNS,DHCPの冗長化は諦めた。 諦めた上で出口のルータとDNS、DHCPは役割を分けた上で出口のルータとWIFIルータのみ冗長化を実施した。 設計方針 過去の失敗から学んだ一番の教訓はやはり「DNS/DHCPの冗長化は難しい」ということだ。 理論上可能ではあるが、私は万年初級自宅インフラエンジニアでいつまで経ってもできる目処が立たない。 それでいて、生成AIという過ぎた兵器を持ってしても、この問題は解決しなかったので、難しいというよりは不可能とした。 2台のサーバでDHCPを冗長化しようとすると、リースの同期問題が必ず発生する。同期ツールを使ったりしたが、どうしても同期しきれなかった。 そこで今回は思い切って諦めた上で役割を分離する方針にした。 ルータ(冗長化) → GWとTailscaleだけ担当 AP → hostapdのみ DNS/DHCP → 専用機1台に集約(冗長化しない) DNSとDHCPは単一障害点になるが、自宅ラボであれば許容範囲だと判断した。 てかどうしようもない。 構成 使用機材はすべてRaspberry Pi。封印していたタワーからも引っ張り出した。 ホスト 役割 ルータA (MASTER) Keepalived + Tailscale ルータB (BACKUP) Keepalived + Tailscale DNS/DHCP機 dnsmasq専用 AP x2 WIFI冗長化 ネットワークセグメントはひとつ。VIPをデフォルトゲートウェイ兼DNSとして各クライアントに配布する。 Keepalivedの設定 まずルータ2台にKeepalivedを入れる。 sudo apt install -y keepalived MASTER側の設定: vrrp_instance VI_1 { state MASTER interface lan-if virtual_router_id 51 priority 100 advert_int 1 authentication { auth_type PASS auth_pass xxxxxxxx } virtual_ipaddress { 192.168.xx.1/24 } } BACKUP側は state BACKUP と priority 90 に変えるだけ。シンプルだ。 ...

April 18, 2026 · 2 min

ドメイン購入後のやり残し作業 - メールとDNS設定編

wasutech.dev を公開したはいいが、メール周りを何もしていなかった。放置するとなりすましに悪用される可能性があるので、Cloudflare Email Routing・SPF・DMARCを設定した。 Cloudflare Email Routing 自前のメールサーバーを建てるのはコストがかかる。Cloudflareには Email Routing という機能があり、@wasutech.dev 宛のメールを既存のGmailなどに転送できる。無料。 仕組みとしては、Cloudflareが自動でMXレコードを追加し、受信したメールを指定のアドレスへ転送する。 dig MX wasutech.dev # → isaac.mx.cloudflare.net 等が返ってくる MXレコードはドメインレベルの情報なので、転送先のメールアドレスは外部に公開されない。dig で見えるのはCloudflareのサーバーだけで、「どこに転送しているか」は誰にもわからない。 設定はCloudflareのダッシュボードから数クリックで完了する。 転送先のアクションは3種類から選べる。 アクション 動作 Forward to email 指定のメールアドレスへ転送 Drop 受信して即破棄。転送しない Send to Worker Cloudflare Workersで独自処理 今回はメールを受け取る必要がないため Drop に設定した。スパム対策にもなるし、転送先アドレスを管理する必要もない。Workers連携は受信メールをSlackに流したり、自動返信を実装したりする場合に使う。 SPF - 送信元を証明するレコード SPFとは SPF(Sender Policy Framework) は、「このドメインからメールを送信していいサーバーはどこか」を定義するDNSレコード。 なぜ必要か。メールのプロトコル(SMTP)は設計上、送信元アドレスを自由に詐称できる。つまり誰でも [email protected] を名乗ってメールを送れる。SPFはこれを受信側が検証できるようにする仕組み。 受信側のメールサーバーは、届いたメールの送信元IPアドレスを確認し、そのドメインのSPFレコードに記載されたIPと照合する。一致しなければ怪しいメールとして処理できる。 設定したレコード v=spf1 include:_spf.mx.cloudflare.net ~all 各要素の意味: 要素 意味 v=spf1 SPFバージョン1 include:_spf.mx.cloudflare.net CloudflareのメールサーバーからのSMTPを許可 ~all 上記以外は「疑わしい」扱い(ソフトフェイル) ~all は疑わしいメールを迷惑メール扱いにする。-all にすると完全拒否になるが、DMARCと組み合わせて制御するのが一般的。 確認: dig TXT wasutech.dev # v=spf1 include:_spf.mx.cloudflare.net ~all DMARC - SPFの結果を使って何をするか決めるレコード DMARCとは DMARC(Domain-based Message Authentication, Reporting, and Conformance) は、SPF(やDKIM)の検証結果に基づいて、受信側が「そのメールをどう扱うか」を指示するレコード。 ...

April 14, 2026 · 1 min