地方エンジニアの学習日記

興味ある技術の雑なメモだったりを書いてくブログ。たまに日記とガジェット紹介。

【障害対応】オレオレインシデントbotツールを書いた

この記事は「渡部 Advent Calendar 2025」の6日目の記事です。


github.com

障害対応の記事を書いたり書籍を書いていたらbotについて聞かれることが多かったのでサンプルとして書いてみたやつ。現在社内で作られたものとは別のものですが概ねこんな感じでやっているというのを紹介してあります。

incident-response-bot は Slack 上で「インシデントを報告・管理」するためのボットです。ざっくり機能ですが

  • Slack でボットをメンションするとモーダル(入力フォーム)が開き、インシデント情報を入力可能 — タイトル、重要度 (Critical / High / Medium / Low)、詳細説明、影響範囲などを記入
  • 入力途中には「〇〇さんが入力中です」のような案内メッセージが表示され、他の人が同時に編集しづらいよう配慮
  • 報告すると、自動でインシデント用チャンネル(例: incident-YYYYMMDD)が作成され、報告内容が整形された形で投稿される
  • 必要に応じて、全体周知用のチャンネルへ同時投稿(通知)も可能。複数の通知先を設定できる
  • インシデント対応ガイドラインの自動投稿、担当者割り当て機能(ハンドラーのアサイン)、および対応履歴の記録(PostgreSQL によるデータ保存)機能 その他、help / list / handler などのコマンドで、オープンインシデントの一覧取得や担当状況の確認も可能

というようなことができます。気づく人もいるかもしれませんが私が過去に所属していた会社で使われていたbotの機能の一部が実装されているという感じです。

speakerdeck.com

こだわっているポイント

障害対応は会社ごとにフローが全く違ったりします。サービスの特性や会社の規模の違いがあるので当然です。なのである程度最初のテンプレートになるような機能だけを実装して必要なものは会社ごとに実装する or 運用でカバーしていくというような使い方を目指しています。

現在は副業先で導入が進んでいて実際の障害対応にも使ってもらっていたりします。ただ社内固有の情報がだんだん増えてきて現在はforkして運用するという形になっています。もう一社でも使えないかは現在検討中で手触り感を見てもらっているという状況でそちらも多分forkするかこんな機能あると良いよ的な感じでスクラッチで書いていくのかは未定です。とりあえずインシデントレスポンスbotがあると障害対応で色々と便利ですよというのを伝えやすくなるというところが達成できているので目的は達成できました。

結局はツールがあっても障害対応に対する姿勢だったり文化の方が大事なのでその部分は別途作っていく必要があったりします。その辺の話は書籍にも書いたので見てもらえると嬉しいですw

gihyo.jp

【Linux】access(2)はなぜ使うべきではないのか

この記事は「渡部 Advent Calendar 2025」の5日目の記事です。


nxmnpg.lemoda.net

ファイルシステムに関わるプログラムを書いていると、access() システムコールを見かける機会は意外と多いと思います。しかし、この access() は “知っておくと便利” というより、「正体を理解して積極的に使うものではない」種類のシステムコールということを当時のエンジニアの先輩に教わりました。特に、セキュリティや SRE、運用観点でファイルアクセスを扱う際には、access() の特性を誤解すると危険な判断につながることがあります。

この記事では、access() の基本から内部動作、なぜ避けるべきとされるのかあたりについて書いていきます。

access() システムコールとは?

access() は 「このプロセスが、指定したファイルに対してパーミッションを持っているか?」 をカーネルに問い合わせるためのシステムコールです。

#include <unistd.h>

int access(const char *pathname, int mode);
  • R_OK … 読み取り可能か?
  • W_OK … 書き込み可能か?
  • X_OK … 実行可能か?
  • F_OK … 存在しているか?

返り値

  • 0 → 要求されたアクセスが可能
  • -1 → 不可能(errno で理由がわかる:EACCES, ENOENT など)

access() と 通常のパーミッションチェック は違う

  1. access() は「実効UID/実効GID」に基づいてチェックする

UNIX のプロセスには以下があります:

  • 実ユーザID(real UID)
  • 実効ユーザID(effective UID) ← アクセス権の基準
  • 保存セットUID

access() は 実効 UID/GID を使ってアクセス可能かどうか判断 します。

sudo で実際に起きる例

sudo -u www-data ./app

この場合:

access() は www-data の権限で判定する

しかし open() は setuid プログラムなら root 権限で実行される可能性がある。つまり access() が通らないのに open() は成功することがあり得ます。

なぜ access() を使うべきでないと言われるのか?

TOCTTOU (Time-of-check vs Time-of-use) 脆弱性

典型例

if (access(path, W_OK) == 0) {
    fd = open(path, O_WRONLY);
}

access() と open() の間に パスの指し示すファイルが変更される可能性 がある。例えば攻撃者がaccess() の直後にファイルを symlink 別のファイルへ付け替えるた状態のopen()は別のファイルを開いてしまう可能性があります。これが classic TOCTTOU attackです。

en.wikipedia.org

カーネル内部ではどう動くのか?

  • sys_access() が呼ばれる
  • パス名の解決(VFS → inode へ)
  • inode_permission() でパーミッションチェック
  • 実効UID/GID を使って所有者・group・others の判定
  • 必要なら capabilities を評価(CAP_DAC_OVERRIDE など)

open() と違って実際にファイルディスクリプタは作られないです。この辺はVFSのレイヤーで実装されているので追うならnfsdあたりが読みやすいかなと思います。

github.com

代わりに何を使うべきか?

ファイルを開きたい:open() を直接使う

int fd = open(path, O_WRONLY);
if (fd < 0) {
    // ここで errno を見る
}

ファイルの属性だけ知りたい:stat()

struct stat st;
stat(path, &st);

実ユーザ権限での判定が必要:faccessat()

より安全で柔軟な *at() ファミリーを使う。

まとめ

  • access() は 「実効ユーザIDでアクセス可能か?」 の問い合わせ
  • TOCTTOU の危険が大きい
  • 実際のファイルアクセス権は open() の結果で判断すべき
  • setuid プログラムなど特殊用途以外では基本的に使わない

明日は渡部さんによる6日目の記事です。楽しみですね。

【Python】スレッドの並列性とGIL

この記事は「渡部 Advent Calendar 2025」の4日目の記事です。


ざっくりいうとCPU の「コア数」を使って、複数のスレッドが同時に物理的に実行されている状態。

  • 「並列(parallel)」=物理的に同時
  • 「並行(concurrent)」=論理的に同時(実際は切り替え)

並列性とは具体的に何を指す?

複数の CPU コアが存在し、それぞれが同時に処理を実行している

たとえば 4 コア CPU だと

Core0: スレッドA を実行
Core1: スレッドB を実行
Core2: スレッドC を実行
Core3: スレッドD を実行

これは 完全な並列実行。4 つのタスクが本当に同時に動いている。

GIL が存在しない言語では、CPU-bound 処理がスレッドで高速化できる

  • C, C++, Go, Java, Rust → どれもスレッドで普通に並列化可能
  • Python でも C拡張が重めの処理(NumPy, OpenCV など)は GIL を release して並列性を獲得できる。

Python は GIL で基本「並列性」がない

Python(CPython)は GIL によ同時に動ける Python bytecode は 1 つだけ。よって Python 純粋コードは「並列」は不可能で「並行」止まり

Thread-1: |----実行----|
Thread-2: |----実行----|

実際は CPU が高速に切り替えているだけ(並行・concurrency)。

GILの実装

GIL は「Python バイトコード実行を 1 スレッドに制限するためのミューテックス」。内部的には以下で構成されています

  • POSIX threads(pthread)や OS ネイティブの mutex
  • 条件変数(Condition Variable)による“GIL の譲渡”メカニズム
  • 「チェック間隔 / タイムスライス」によるスレッド切り替えの仕組み
  • GIL の保持・解放を自動管理するマクロ(PyGILState_Ensure など)

GIL の状態は _gil_runtime_state という構造体で管理されている。

typedef struct {
    PyMutex mutex;            // GIL 本体(排他ロック)
    PyCondVar cond;           // スレッド切り替え用の条件変数
    int locked;               // ロックされているか?
    PyThreadState *owner;     // GIL を保持しているスレッド
    _Py_atomic_address last_holder; // デバッグ用
    int switch_number;        // スレッド切り替え回数
} _gil_runtime_state;

スレッドはどうやって GIL を取っているのか?

Python のスレッド(PyThreadState)はバイトコードを実行する前に 必ず GIL の取得処理を行う。

内部コードでは次のように mutex を lock する。

PyMutex_Lock(&gil.mutex);
gil.locked = 1;
gil.owner = PyThreadState_GET();

これでそのスレッドは Python バイトコードを実行できる。

GILの解放

自動解放メカニズム

In order to emulate concurrency of execution, the interpreter regularly
tries to switch threads (see :func:`sys.setswitchinterval`).  The lock is also
released around potentially blocking I/O operations like reading or writing
a file, so that other Python threads can run in the meantime.

ブロッキングI/O操作時

GILはファイル読み書きなどのブロッキングI/O操作の前後に自動的に解放・再取得されます init.rst:784-786 。これによりI/O待機中に他のスレッドがPythonコードを実行できます。以下はIO操作時のスレッドの解放処理です。

static int
_enter_buffered_busy(buffered *self)
{
    int relax_locking;
    PyLockStatus st;
    if (self->owner == PyThread_get_thread_ident()) {
        PyErr_Format(PyExc_RuntimeError,
                     "reentrant call inside %R", self);
        return 0;
    }
    PyInterpreterState *interp = _PyInterpreterState_GET();
    relax_locking = _Py_IsInterpreterFinalizing(interp);
    Py_BEGIN_ALLOW_THREADS
    if (!relax_locking)
        st = PyThread_acquire_lock(self->lock, 1);
    else {
        /* When finalizing, we don't want a deadlock to happen with daemon
         * threads abruptly shut down while they owned the lock.
         * Therefore, only wait for a grace period (1 s.).
         * Note that non-daemon threads have already exited here, so this
         * shouldn't affect carefully written threaded I/O code.
         */
        st = PyThread_acquire_lock_timed(self->lock, (PY_TIMEOUT_T)1e6, 0);
    }
    Py_END_ALLOW_THREADS
    if (relax_locking && st != PY_LOCK_ACQUIRED) {
        PyObject *ascii = PyObject_ASCII((PyObject*)self);
        _Py_FatalErrorFormat(__func__,
            "could not acquire lock for %s at interpreter "
            "shutdown, possibly due to daemon threads",
            ascii ? PyUnicode_AsUTF8(ascii) : "<ascii(self) failed>");
    }
    return 1;
}

タイムスライスによる強制スイッチ

sys.setswitchinterval()で設定された間隔(デフォルト5ms)で強制的にスレッド切り替えが発生し、GILが解放されます

手動解放メカニズム

C 拡張で GIL を解放する処理

// example.c
#define PY_SSIZE_T_CLEAN
#include <Python.h>
#include <unistd.h>  // sleep()

// Python 側から呼び出される関数: sleep(seconds) するだけ
static PyObject* my_sleep(PyObject* self, PyObject* args)
{
    int seconds;

    if (!PyArg_ParseTuple(args, "i", &seconds)) {
        return NULL;
    }

    // ここから GIL を解放
    Py_BEGIN_ALLOW_THREADS

    // ---- ここは GIL なしで実行される領域 ----
    // ・他スレッドの Python コードと真の並列実行が可能
    // ・ただし Python オブジェクトへのアクセスは禁止
    sleep(seconds);
    // -------------------------------------

    // ここで GIL を再取得
    Py_END_ALLOW_THREADS

    Py_RETURN_NONE;
}

// モジュール定義
static PyMethodDef ExampleMethods[] = {
    {"my_sleep", my_sleep, METH_VARARGS, "Sleep without holding the GIL"},
    {NULL, NULL, 0, NULL}
};

static struct PyModuleDef examplemodule = {
    PyModuleDef_HEAD_INIT,
    "example",
    NULL,
    -1,
    ExampleMethods
};

PyMODINIT_FUNC PyInit_example(void)
{
    return PyModule_Create(&examplemodule);
}

OSのプリエンプションとGILのタイムスライスは異なる抽象レベルで動作する

Linux でも macOS でも Windows でも、スレッドは OS によってプリエンプティブにスケジュールされます。つまりスレッドAが CPU を使っていても、OS は強制的に A を止めて B に切り替えるこれは Python だろうと Java だろうと C だろうと同じでPython は OS のスケジューラを変更することはできないです。

相互作用の詳細

  • 実行フロー
    • OSがPythonスレッドをCPUにスケジュール
    • スレッドがGILを取得してPythonバイトコードを実行
    • OSがスレッドをプリエンプトしても、GILは保持されたまま
    • スレッドが再開されると、引き続きGILを持って実行を継続
    • setswitchintervalの時間が経過すると、GILが解放されて他のPythonスレッドに譲渡

重要な違いとしてはこんな感じ。

まとめ

  • 並列(parallel)=CPU の複数コアで“物理的に同時実行”。
  • 並行(concurrent)=実際は1つを高速に切り替えて“同時に見えるだけ”。

Python(CPython)は GIL により1つのスレッドしか Python バイトコードを実行できないため、基本は「並行」止まりで「並列」はできない。

GIL は OS の mutex+条件変数で実装され、I/O や C拡張では 自動的に解放されるため、その間は並列になる。

OS のプリエンプション(強制切替)とGIL のスレッド切替(協調動作)はまったく別レイヤー。

【ポエム】対話は難しい

この記事は「渡部 Advent Calendar 2025」の3日目の記事です。

私は対話が苦手だ。みたいな話ではなく対話って難しい営みだよなと思った話です。難しいというか面白いなと思ったポイントを書いてみます。

アドカレ2日続けてポエムかよって思いもあるが溜まっていたストックなので消化していく。

タスク指向型と非タスク指向型がある

「対話」は、大きく分けて2種類があります。ひとつはタスク指向型対話。もうひとつは非タスク指向型対話です。

タスク指向型は「目的を達成するための会話」です。たとえば「レストランを予約する」「サーバの障害を報告する」「買い物リストを作る」といった、 明確なゴールを共有した上で情報を交換するタイプです。一方で、非タスク指向型は「目的のない会話」です。雑談、相談、共感、愚痴、アイスブレイクなどがこれに当たります。ゴールは定義されず、むしろ会話そのものが目的となる。

この2種類の対話は、心理的にも文化的にも異なりますが、技術的に見てもまったく別の難しさを持っています。タスク指向型は「構造化されているからこそ難しい」。非タスク指向型は「構造化されていないから難しい」。いずれも人間の自然な対話を再現しようとすると、システム設計というか実装がとてつもなく難しいということ気づきます。

ちなみにこの辺を体系だって学ぼうとすると大学の教科書みたいなのがたくさん出てきます。そしてもれなく数学がついてきます。

対話システムの作り方 | 近代科学社

Pythonでつくる対話システム | Ohmsha

現実世界とのインターフェースの複雑さ

タスク指向型の対話システムは、単に言葉を理解するだけではなく、外部システムとの双方向連携が必須です。たとえば「明日の19時に渋谷でイタリアンを予約して」と言われた場合、内部的には次のような処理が発生します。

  • エンティティ抽出(意図・場所・時間・料理ジャンルの検出)
  • スロット補完(未指定パラメータの確認:「人数は何名ですか?」)
  • 外部API呼び出し(予約DB・在庫システムなど)
  • エラー処理(満席・通信失敗・入力不備など)
  • 状態更新とレスポンス生成

これらは単なる自然言語処理ではなく、トランザクション制御・状態遷移管理・例外処理を含む複合的な問題です。つまり、対話はAPI設計や分散システム設計と同質の難易度を持つことになります。

状態管理の爆発的複雑性

タスク指向型では「対話状態」を常に維持しなければなりません。しかし、ユーザーが一度に複数のスロットを更新したり、順序を入れ替えたり、途中で意図を変えたりすると、状態空間が爆発します。

例:
「やっぱり6時じゃなくて7時にして。場所も新宿の方がいいかも。」

このような修正発話は、単純な状態遷移表では表現できません。また、「6時→7時に変更」という操作は差分更新を要するため、自然言語理解と状態管理を密結合させる必要があります。この結合部のモデリングが非常に難しく、わずかな設計ミスで「状態の破綻」が起きることになります。以下はこの辺を考慮せずに実装してる例です。状態数が指数関数的に爆発していくのがわかりやすいかなと思います。

「変更したいスロットの組み合わせ」を全部状態に持つナイーブ実装

from enum import Enum, auto
from dataclasses import dataclass
from typing import Optional


class DialogState(Enum):
    START = auto()
    FILLING_SLOTS = auto()
    CONFIRM = auto()

    # 変更系(地獄の入り口)
    CHANGE_TIME = auto()
    CHANGE_PLACE = auto()
    CHANGE_PEOPLE = auto()
    CHANGE_BUDGET = auto()

    CHANGE_TIME_PLACE = auto()
    CHANGE_TIME_PEOPLE = auto()
    CHANGE_TIME_BUDGET = auto()
    CHANGE_PLACE_PEOPLE = auto()
    CHANGE_PLACE_BUDGET = auto()
    CHANGE_PEOPLE_BUDGET = auto()

    CHANGE_TIME_PLACE_PEOPLE = auto()
    CHANGE_TIME_PLACE_BUDGET = auto()
    CHANGE_TIME_PEOPLE_BUDGET = auto()
    CHANGE_PLACE_PEOPLE_BUDGET = auto()

    CHANGE_TIME_PLACE_PEOPLE_BUDGET = auto()


@dataclass
class BookingSlots:
    time: Optional[str] = None
    place: Optional[str] = None
    people: Optional[int] = None
    budget: Optional[int] = None


@dataclass
class DialogContext:
    state: DialogState
    slots: BookingSlots


def handle_user_utterance(ctx: DialogContext, utterance: str) -> DialogContext:
    """
    超ナイーブな実装例。
    「やっぱり時間変えて / 場所も… / 予算も…」みたいな修正発話を
    すべて状態として分岐させようとしたらどうなるか、をあえて書いたもの。
    """

    u = utterance

    # 例: 変更意図検出(あり得ないくらい雑な実装)
    wants_time = "時間" in u or "時" in u
    wants_place = "場所" in u or "新宿" in u or "渋谷" in u
    wants_people = "人数" in u or "人" in u
    wants_budget = "予算" in u or "円" in u

    # 組み合わせ爆発
    if wants_time and not wants_place and not wants_people and not wants_budget:
        ctx.state = DialogState.CHANGE_TIME
    elif wants_place and not wants_time and not wants_people and not wants_budget:
        ctx.state = DialogState.CHANGE_PLACE
    elif wants_people and not wants_time and not wants_place and not wants_budget:
        ctx.state = DialogState.CHANGE_PEOPLE
    elif wants_budget and not wants_time and not wants_place and not wants_people:
        ctx.state = DialogState.CHANGE_BUDGET

    elif wants_time and wants_place and not wants_people and not wants_budget:
        ctx.state = DialogState.CHANGE_TIME_PLACE
    elif wants_time and wants_people and not wants_place and not wants_budget:
        ctx.state = DialogState.CHANGE_TIME_PEOPLE
    elif wants_time and wants_budget and not wants_place and not wants_people:
        ctx.state = DialogState.CHANGE_TIME_BUDGET
    elif wants_place and wants_people and not wants_time and not wants_budget:
        ctx.state = DialogState.CHANGE_PLACE_PEOPLE
    elif wants_place and wants_budget and not wants_time and not wants_people:
        ctx.state = DialogState.CHANGE_PLACE_BUDGET
    elif wants_people and wants_budget and not wants_time and not wants_place:
        ctx.state = DialogState.CHANGE_PEOPLE_BUDGET

    elif wants_time and wants_place and wants_people and not wants_budget:
        ctx.state = DialogState.CHANGE_TIME_PLACE_PEOPLE
    elif wants_time and wants_place and wants_budget and not wants_people:
        ctx.state = DialogState.CHANGE_TIME_PLACE_BUDGET
    elif wants_time and wants_people and wants_budget and not wants_place:
        ctx.state = DialogState.CHANGE_TIME_PEOPLE_BUDGET
    elif wants_place and wants_people and wants_budget and not wants_time:
        ctx.state = DialogState.CHANGE_PLACE_PEOPLE_BUDGET

    elif wants_time and wants_place and wants_people and wants_budget:
        ctx.state = DialogState.CHANGE_TIME_PLACE_PEOPLE_BUDGET

    # …ここからさらに「今は time だけ埋まっている状態で change_place が来た場合」
    # みたいな条件を足し始めると、本当に終わらない if-else ツリーになる

    return ctx

エラーハンドリングの難易度

タスク指向型では、成功/失敗が明確に定義されている分だけ、失敗時のリカバリ戦略が重要になります。

  • 外部APIが落ちている場合にどう案内するか
  • 必須スロットが永遠に埋まらない場合にどうフェイルするか
  • 同じ情報を繰り返し要求されたときにどう応答を変えるか

これらは人間なら「空気を読んで」対応できますが、システムでは逐次的に条件分岐を設けなければならない。その結果、if文の森が形成され、保守性が急速に悪化します。

if intent == "reserve":
    if api_down("restaurant"):
        if user_said("急いでる"):
            apologize_fast()
        else:
            fallback_suggestion()
    else:
        if not slots.time:
            if inferred_time_from_history():
                fill_from_context()
            else:
                ask_time()
        elif not slots.people:
            ask_people()
        else:
            if is_available(slots):
                if user_said("やっぱりキャンセル"):
                    cancel_flow()
                else:
                    confirm()
            else:
                if user_said("別の場所で探して"):
                    search_area(user_specified_or_default())
                else:
                    ask_alternative_time()
else:
    if intent == "smalltalk":
        handle_chitchat()
    else:
        fallback()

例の波動拳にも見えてきそうですね。

人間の曖昧さとルールの衝突

「タスク」とはいえ、人間の発話は常に曖昧です。「安いやつ」「近いところ」「いい感じの」といった定性的表現は、どんなにスロットを定義しても欠落します。これを解決しようとすると、

  • 辞書ベース+確率モデルのハイブリッド化
  • NLUの信頼度スコアに基づくフォールバック戦略
  • 対話履歴による再解釈(contextual re-interpretation)

などの高度な仕組みが必要になり、最終的には言語理解+計画立案+例外処理の総合問題に行き着きます。

まとめ

対話する時って実はこんなにも高度な営みが裏では行われていたというのを対話システムに携わることで気づきました。これを全てのパターンで実装しようとすると凄まじい計算量になってレスポンスが遅すぎて会話にならないんじゃないかなと思います。これをいい感じにしていく対話システムって本当にすごいですよ。


明日は渡部さんによる4日目の記事です。何が書かれるんでしょうかね。