• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

kimata / my-py-lib / 20836463957

09 Jan 2026 12:14AM UTC coverage: 63.628% (-1.3%) from 64.892%
20836463957

push

github

kimata
fix: webapp 自動インポートを削除し Flask 依存を遅延化

my_lib をインポートしただけで Flask が必要になる問題を修正。
webapp サブモジュールは必要な時のみ個別にインポートするようにした。

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

3343 of 5254 relevant lines covered (63.63%)

0.64 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

99.23
/src/my_lib/sqlite_util.py
1
#!/usr/bin/env python3
2
"""SQLiteデータベースのユーティリティ関数
3

4
Kubernetes manual storage class(ローカルストレージ/NFS)環境に最適化。
5

6
環境変数による設定:
7
    SQLITE_JOURNAL_MODE: ジャーナルモード (WAL/DELETE/TRUNCATE/PERSIST) デフォルト: WAL
8
    SQLITE_MMAP_SIZE: mmapサイズ(バイト単位、0で無効化)デフォルト: 0
9
    SQLITE_LOCKING_MODE: ロックモード (NORMAL/EXCLUSIVE) デフォルト: NORMAL
10
    SQLITE_LOCK_MODE: fcntlロックモード (BLOCK/NONBLOCK) デフォルト: BLOCK
11
    SQLITE_CHECKPOINT_DIR: WALチェックポイント用ディレクトリ
12
"""
13

14
from __future__ import annotations
1✔
15

16
import contextlib
1✔
17
import fcntl
1✔
18
import logging
1✔
19
import os
1✔
20
import pathlib
1✔
21
import sqlite3
1✔
22
import time
1✔
23
from typing import Any
1✔
24

25

26
def init(conn: sqlite3.Connection, *, timeout: float = 60.0) -> None:
1✔
27
    """
28
    SQLiteデータベースのテーブル設定を初期化する
29

30
    Args:
31
        conn: SQLiteデータベース接続
32
        timeout: データベース接続のタイムアウト時間(秒)
33

34
    """
35
    # Kubernetes manual storage class(ローカルストレージ/NFS)に最適化されたPRAGMA設定
36

37
    # ジャーナルモード: WALモードはNFSでも動作するが、DELETEモードの方が安全な場合もある
38
    # NFSでのファイルロック問題を回避するため、環境に応じて選択可能にする
39
    journal_mode = os.environ.get("SQLITE_JOURNAL_MODE", "WAL")
1✔
40
    conn.execute(f"PRAGMA journal_mode={journal_mode}")
1✔
41

42
    # 同期モード: NFSやローカルストレージではFULLが最も安全
43
    # データ整合性を優先(特にNFSの場合)
44
    conn.execute("PRAGMA synchronous=FULL")
1✔
45

46
    # ページサイズ: 標準的な4096バイトを使用(ほとんどのファイルシステムで最適)
47
    conn.execute("PRAGMA page_size=4096")
1✔
48

49
    # WALモード使用時の設定
50
    if journal_mode == "WAL":
1✔
51
        # WALの自動チェックポイント間隔(デフォルト1000)
52
        conn.execute("PRAGMA wal_autocheckpoint=1000")
1✔
53

54
        # WALファイルの最大サイズを制限(NFSでの巨大ファイル転送を避ける)
55
        conn.execute("PRAGMA journal_size_limit=67108864")  # 64MB
1✔
56

57
    # キャッシュサイズ: 控えめに設定(Pod のメモリ制限を考慮)
58
    conn.execute("PRAGMA cache_size=-32000")  # 約32MB(負値はKB単位)
1✔
59

60
    # テンポラリストレージをメモリに設定(ディスクI/Oを削減)
61
    conn.execute("PRAGMA temp_store=MEMORY")
1✔
62

63
    # mmapサイズ: NFSでは無効化または小さく設定(0で無効化)
64
    # NFSでのmmapは問題を起こす可能性があるため
65
    mmap_size = int(os.environ.get("SQLITE_MMAP_SIZE", "0"))
1✔
66
    conn.execute(f"PRAGMA mmap_size={mmap_size}")
1✔
67

68
    # ロックタイムアウトを長めに設定(NFSレイテンシを考慮)
69
    conn.execute(f"PRAGMA busy_timeout={int(timeout * 1000)}")
1✔
70

71
    # 外部キー制約を有効化(データ整合性のため)
72
    conn.execute("PRAGMA foreign_keys=ON")
1✔
73

74
    # ロックモード: NFSでは排他ロックモードが推奨される場合がある
75
    locking_mode = os.environ.get("SQLITE_LOCKING_MODE", "NORMAL")
1✔
76
    if locking_mode == "EXCLUSIVE":
1✔
77
        conn.execute("PRAGMA locking_mode=EXCLUSIVE")
1✔
78

79
    conn.commit()
1✔
80
    logging.info("SQLiteデータベースのテーブル設定を初期化しました")
1✔
81

82

83
class DatabaseConnection:
1✔
84
    """SQLite接続をContext Managerとしても通常の関数としても使用可能にするラッパー"""
85

86
    def __init__(self, db_path: str | pathlib.Path, *, timeout: float = 60.0) -> None:
1✔
87
        """
88
        データベース接続の初期化
89

90
        Args:
91
            db_path: データベースファイルのパス
92
            timeout: データベース接続のタイムアウト時間(秒)
93

94
        """
95
        self.db_path = pathlib.Path(db_path)
1✔
96
        self.timeout = timeout
1✔
97
        self.conn: sqlite3.Connection | None = None
1✔
98

99
    def _acquire_lock(self, lock_file: Any) -> bool:
1✔
100
        """ロックの取得を試みる"""
101
        if os.environ.get("SQLITE_LOCK_MODE") == "NONBLOCK":
1✔
102
            try:
1✔
103
                fcntl.flock(lock_file.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
1✔
104
                return True
1✔
105
            except BlockingIOError:
1✔
106
                return False
1✔
107
        else:
108
            # 通常の排他ロック(ブロッキング)
109
            fcntl.flock(lock_file.fileno(), fcntl.LOCK_EX)
1✔
110
            return True
1✔
111

112
    def _get_connection_params(self) -> dict[str, Any]:
1✔
113
        """SQLite接続パラメータを取得"""
114
        params: dict[str, Any] = {
1✔
115
            "timeout": self.timeout,
116
            "check_same_thread": False,
117
            "isolation_level": "DEFERRED",
118
        }
119

120
        # NFSキャッシュ対策: checkpointディレクトリが使われる場合の設定
121
        checkpoint_dir = os.environ.get("SQLITE_CHECKPOINT_DIR")
1✔
122
        if checkpoint_dir:
1✔
123
            pathlib.Path(checkpoint_dir).mkdir(parents=True, exist_ok=True)
1✔
124

125
        return params
1✔
126

127
    def _create_connection(self) -> sqlite3.Connection:
1✔
128
        """実際の接続処理"""
129
        self.db_path.parent.mkdir(parents=True, exist_ok=True)
1✔
130

131
        # データベースファイルが存在しない場合のみ初期化を実行
132
        is_new_db = not self.db_path.exists()
1✔
133

134
        if is_new_db:
1✔
135
            # 新規作成時は排他制御を行う
136
            lock_path = self.db_path.with_suffix(".lock")
1✔
137
            max_retries = 5
1✔
138
            retry_count = 0
1✔
139

140
            while retry_count < max_retries:
1✔
141
                try:
1✔
142
                    # ロックファイルを使用して排他制御
143
                    with lock_path.open("w") as lock_file:
1✔
144
                        if not self._acquire_lock(lock_file):
1✔
145
                            retry_count += 1
1✔
146
                            time.sleep(0.1 * retry_count)  # 指数バックオフ
1✔
147
                            continue
1✔
148

149
                        try:
1✔
150
                            # ロック取得後、再度存在確認(他のプロセスが作成済みの可能性)
151
                            is_new_db = not self.db_path.exists()
1✔
152
                            self.conn = sqlite3.connect(self.db_path, **self._get_connection_params())
1✔
153

154
                            if is_new_db:
1✔
155
                                init(self.conn, timeout=self.timeout)
1✔
156
                                logging.info("新規SQLiteデータベースを作成・初期化しました: %s", self.db_path)
1✔
157
                            else:
158
                                logging.debug(
×
159
                                    "既存のSQLiteデータベースに接続しました(ロック待機後): %s", self.db_path
160
                                )
161
                        finally:
162
                            # ロックを解放
163
                            fcntl.flock(lock_file.fileno(), fcntl.LOCK_UN)
1✔
164

165
                    # ロックファイルを削除(エラーは無視)
166
                    with contextlib.suppress(Exception):
1✔
167
                        lock_path.unlink()
1✔
168
                    break  # 成功したらループを抜ける
1✔
169

170
                except Exception:
1✔
171
                    retry_count += 1
1✔
172
                    if retry_count >= max_retries:
1✔
173
                        logging.exception("データベース作成時のロック取得に失敗しました")
1✔
174
                        raise
1✔
175
                    time.sleep(0.1 * retry_count)
1✔
176
        else:
177
            # 既存のデータベースへの接続
178
            self.conn = sqlite3.connect(self.db_path, **self._get_connection_params())
1✔
179
            logging.debug("既存のSQLiteデータベースに接続しました: %s", self.db_path)
1✔
180

181
        assert self.conn is not None  # noqa: S101
1✔
182
        return self.conn
1✔
183

184
    def __enter__(self) -> sqlite3.Connection:
1✔
185
        """Context Manager として使用する場合のenter"""
186
        return self._create_connection()
1✔
187

188
    def __exit__(
1✔
189
        self,
190
        exc_type: type[BaseException] | None,
191
        exc_val: BaseException | None,
192
        exc_tb: Any,
193
    ) -> None:
194
        """Context Manager として使用する場合のexit"""
195
        if self.conn is not None:
1✔
196
            if exc_type is None:
1✔
197
                self.conn.commit()  # 正常終了時はコミット
1✔
198
            else:
199
                self.conn.rollback()  # 例外発生時はロールバック
1✔
200
            self.conn.close()
1✔
201

202
    def get(self) -> sqlite3.Connection:
1✔
203
        """通常の関数として使用する場合(使用後は必ずcloseすること)"""
204
        return self._create_connection()
1✔
205

206

207
def connect(db_path: str | pathlib.Path, *, timeout: float = 60.0) -> DatabaseConnection:
1✔
208
    """
209
    Kubernetes manual storage class環境に適したSQLiteデータベースに接続する
210

211
    Context Managerとしても通常の関数としても使用可能
212

213
    Args:
214
        db_path: データベースファイルのパス
215
        timeout: データベース接続のタイムアウト時間(秒)
216

217
    Returns:
218
        DatabaseConnection: Context Managerとしても通常の接続取得にも使用可能
219

220
    Usage:
221
        # Context Manager として使用
222
        with my_lib.sqlite_util.connect(db_path) as conn:
223
            conn.execute("SELECT * FROM table")
224

225
        # 通常の関数として使用
226
        db_conn = my_lib.sqlite_util.connect(db_path)
227
        conn = db_conn.get()
228
        try:
229
            conn.execute("SELECT * FROM table")
230
        finally:
231
            conn.close()
232

233
    """
234
    return DatabaseConnection(db_path, timeout=timeout)
1✔
235

236

237
def recover(db_path: str | pathlib.Path) -> None:
1✔
238
    """データベースの復旧を試みる"""
239
    try:
1✔
240
        db_path = pathlib.Path(db_path)
1✔
241

242
        # ジャーナルファイルのパスを取得
243
        journal_mode = os.environ.get("SQLITE_JOURNAL_MODE", "WAL")
1✔
244

245
        if journal_mode == "WAL":
1✔
246
            # WALモードの場合
247
            wal_path = db_path.with_suffix(db_path.suffix + "-wal")
1✔
248
            shm_path = db_path.with_suffix(db_path.suffix + "-shm")
1✔
249

250
            if wal_path.exists():
1✔
251
                logging.warning("WALファイル %s を削除してデータベースを復旧します", wal_path)
1✔
252
                wal_path.unlink()
1✔
253

254
            if shm_path.exists():
1✔
255
                logging.warning("共有メモリファイル %s を削除します", shm_path)
1✔
256
                shm_path.unlink()
1✔
257
        else:
258
            # その他のジャーナルモードの場合
259
            journal_path = db_path.with_suffix(db_path.suffix + "-journal")
1✔
260
            if journal_path.exists():
1✔
261
                logging.warning("ジャーナルファイル %s を削除してデータベースを復旧します", journal_path)
1✔
262
                journal_path.unlink()
1✔
263

264
        # データベースの整合性チェック
265
        try:
1✔
266
            conn = sqlite3.connect(db_path, timeout=5.0)
1✔
267
            result = conn.execute("PRAGMA integrity_check").fetchone()
1✔
268
            if result[0] != "ok":
1✔
269
                raise sqlite3.DatabaseError(f"整合性チェック失敗: {result[0]}")
1✔
270

271
            # 追加のチェック: quick_check(より高速)
272
            conn.execute("PRAGMA quick_check")
1✔
273

274
            # VACUUM実行(データベースの最適化と修復)
275
            if os.environ.get("SQLITE_AUTO_VACUUM") == "1":
1✔
276
                logging.info("データベースのVACUUMを実行します")
1✔
277
                conn.execute("VACUUM")
1✔
278

279
            conn.close()
1✔
280
            logging.info("データベースの整合性チェックが成功しました")
1✔
281

282
        except Exception:
1✔
283
            logging.exception("データベースの整合性チェックに失敗")
1✔
284
            # 最後の手段としてデータベースを再作成
285
            backup_path = db_path.with_suffix(f".backup.{int(time.time())}")
1✔
286
            db_path.rename(backup_path)
1✔
287
            logging.warning("破損したデータベースを %s にバックアップし、新規作成します", backup_path)
1✔
288

289
    except Exception:
1✔
290
        logging.exception("データベース復旧中にエラーが発生")
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc