2026年2月9日月曜日

外部の Redis にアクセスする場合はちゃんと pipeline を使おう

外部の Redis にアクセスする場合はちゃんと pipeline を使おう

概要

ループごとに GET するような処理はネットワークの負荷が重くなり結果としてアプリのレスポンス遅延にもつながるので可能な限り1回でほしい結果はすべて取得するようにしましょう

環境

  • Python 3.12.11
  • redis-py 7.1.0
  • redis 8.4.0

ダメなコード

def get_index_pairs(redis_client, hand_images: dict, ai_images: dict) -> list:
    profiles = get_profiles()
    pairs = []
    for num in sorted(
        set(hand_images.keys()) | set(ai_images.keys()),
        key=lambda x: int(x),
        reverse=True,
    ):
        hand_like_key = f"{num}_hand"
        ai_like_key = f"{num}_ai"
        hand_like = int(redis_client.get(hand_like_key) or 0)  # type: ignore
        ai_like = int(redis_client.get(ai_like_key) or 0)  # type: ignore
        pair = {
            "num": num,
            "hand": hand_images.get(num),
            "ai": ai_images.get(num),
            "hand_like": hand_like,
            "ai_like": ai_like,
            "name_hand": profiles.get(hand_like_key, {}).get("name", "名無し"),
            "character_hand": profiles.get(hand_like_key, {}).get(
                "character", "考え中"
            ),
            "name_ai": profiles.get(ai_like_key, {}).get("name", "名無し"),
            "character_ai": profiles.get(ai_like_key, {}).get("character", "考え中"),
            "winner": (
                "hand" if hand_like > ai_like else "ai" if ai_like > hand_like else None
            ),
        }
        pairs.append(pair)
    return pairs

良いコード

def get_index_pairs(redis_client, hand_images: dict, ai_images: dict) -> list:
    profiles = get_profiles()
    sorted_nums = sorted(
        set(hand_images.keys()) | set(ai_images.keys()),
        key=lambda x: int(x),
        reverse=True,
    )

    # --- Pipeline の開始 ---
    pipe = redis_client.pipeline()
    for num in sorted_nums:
        pipe.get(f"{num}_hand")
        pipe.get(f"{num}_ai")

    # 1回の通信で全データを取得
    raw_results = pipe.execute()
    # --- Pipeline の終了 ---

    pairs = []
    # 取得した結果を 2 つずつ取り出す
    for i, num in enumerate(sorted_nums):
        hand_like_bytes = raw_results[i * 2]
        ai_like_bytes = raw_results[i * 2 + 1]

        # decode_responses=False の場合を考慮
        hand_like = int(hand_like_bytes.decode() if hand_like_bytes else 0)
        ai_like = int(ai_like_bytes.decode() if ai_like_bytes else 0)

        pair = {
            "num": num,
            "hand": hand_images.get(num),
            "ai": ai_images.get(num),
            "hand_like": hand_like,
            "ai_like": ai_like,
            "name_hand": profiles.get(f"{num}_hand", {}).get("name", "名無し"),
            "character_hand": profiles.get(f"{num}_hand", {}).get(
                "character", "考え中"
            ),
            "name_ai": profiles.get(f"{num}_ai", {}).get("name", "名無し"),
            "character_ai": profiles.get(f"{num}_ai", {}).get("character", "考え中"),
            "winner": (
                "hand" if hand_like > ai_like else "ai" if ai_like > hand_like else None
            ),
        }
        pairs.append(pair)
    return pairs

比較

ネットワーク・レイテンシの削減

これが最も大きな改善点です。

ダメなコード(逐次処理): ループの中で毎回 redis_client.get() を呼び出しています。もし num が100個あれば、200回の通信(リクエストとレスポンスの往復)が発生します。

良いコード(パイプライン): すべての get コマンドを一旦溜めてから、pipe.execute() で 1回だけ Redisサーバーに送ります。通信回数が1回(往復1回)で済むため、ネットワークの遅延(レイテンシ)の影響を最小限に抑えられます。

スループット(処理能力)の向上

Redisサーバー側の負荷も軽減されます。

ダメなコード: サーバーは「リクエスト受信 → 解析 → 実行 → レスポンス送信」というサイクルを200回繰り返します。

良いコード: サーバーは「大きなリクエストを1つ受信 → まとめて実行 → 大きなレスポンスを1つ送信」という挙動になります。これにより、コンテキストスイッチやパケット処理のオーバーヘッドが減り、全体のスループットが向上します。

型の安全性と堅牢な実装

細かい部分ですが、データの扱いも丁寧になっています。

デコード処理: hand_like_bytes.decode() if hand_like_bytes else 0 のように、Redisから返ってくる値が None(キーが存在しない)場合や、バイト列(bytes型)である可能性を考慮して明示的にデコードしています。

マジックナンバーの排除: i * 2 と i * 2 + 1 を使ってパイプラインの結果から正しくデータを取り出しており、インデックス管理が明確です。

最後に

開発初期ではデータが少なく問題にならないですがデータが増えてくるとアプリの挙動がおかしくなるので注意しましょう

gunicorn などを使っていると Redis の応答よりもワーカーが先に死亡し以下のようなエラーになります

Traceback (most recent call last):
  File "/Users/username/.local/share/virtualenvs/ai-gallery-nT0ArqSL/lib/python3.12/site-packages/gunicorn/workers/sync.py", line 142, in handle
    self.handle_request(listener, req, client, addr)
  File "/Users/username/.local/share/virtualenvs/ai-gallery-nT0ArqSL/lib/python3.12/site-packages/gunicorn/workers/sync.py", line 185, in handle_request
    respiter = self.wsgi(environ, resp.start_response)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/username/.local/share/virtualenvs/ai-gallery-nT0ArqSL/lib/python3.12/site-packages/flask/app.py", line 1536, in __call__
    return self.wsgi_app(environ, start_response)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/username/.local/share/virtualenvs/ai-gallery-nT0ArqSL/lib/python3.12/site-packages/werkzeug/middleware/proxy_fix.py", line 183, in __call__
    return self.app(environ, start_response)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/username/.local/share/virtualenvs/ai-gallery-nT0ArqSL/lib/python3.12/site-packages/flask/app.py", line 1511, in wsgi_app
    response = self.full_dispatch_request()
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/username/.local/share/virtualenvs/ai-gallery-nT0ArqSL/lib/python3.12/site-packages/flask/app.py", line 917, in full_dispatch_request
    rv = self.dispatch_request()
         ^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/username/.local/share/virtualenvs/ai-gallery-nT0ArqSL/lib/python3.12/site-packages/flask/app.py", line 902, in dispatch_request
    return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args)  # type: ignore[no-any-return]
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/username/data/repo/ai-gallery/app.py", line 162, in index
    pairs = get_pairs()
            ^^^^^^^^^^^
  File "/Users/username/data/repo/ai-gallery/app.py", line 143, in get_pairs
    pairs = get_index_pairs(redis_client, hand_images, ai_images)
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/username/data/repo/ai-gallery/lib/pair.py", line 45, in get_index_pairs
    ai_like = int(redis_client.get(ai_like_key) or 0)  # type: ignore
                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/username/.local/share/virtualenvs/ai-gallery-nT0ArqSL/lib/python3.12/site-packages/redis/commands/core.py", line 1923, in get
    return self.execute_command("GET", name, keys=[name])
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/username/.local/share/virtualenvs/ai-gallery-nT0ArqSL/lib/python3.12/site-packages/redis/client.py", line 657, in execute_command
    return self._execute_command(*args, **options)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/username/.local/share/virtualenvs/ai-gallery-nT0ArqSL/lib/python3.12/site-packages/redis/client.py", line 668, in _execute_command
    return conn.retry.call_with_retry(
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/username/.local/share/virtualenvs/ai-gallery-nT0ArqSL/lib/python3.12/site-packages/redis/retry.py", line 116, in call_with_retry
    return do()
           ^^^^
  File "/Users/username/.local/share/virtualenvs/ai-gallery-nT0ArqSL/lib/python3.12/site-packages/redis/client.py", line 669, in <lambda>
    lambda: self._send_command_parse_response(
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/username/.local/share/virtualenvs/ai-gallery-nT0ArqSL/lib/python3.12/site-packages/redis/client.py", line 640, in _send_command_parse_response
    return self.parse_response(conn, command_name, **options)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/username/.local/share/virtualenvs/ai-gallery-nT0ArqSL/lib/python3.12/site-packages/redis/client.py", line 691, in parse_response
    response = connection.read_response()
               ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/username/.local/share/virtualenvs/ai-gallery-nT0ArqSL/lib/python3.12/site-packages/redis/connection.py", line 1133, in read_response
    response = self._parser.read_response(disable_decoding=disable_decoding)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/username/.local/share/virtualenvs/ai-gallery-nT0ArqSL/lib/python3.12/site-packages/redis/_parsers/resp2.py", line 15, in read_response
    result = self._read_response(disable_decoding=disable_decoding)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/username/.local/share/virtualenvs/ai-gallery-nT0ArqSL/lib/python3.12/site-packages/redis/_parsers/resp2.py", line 25, in _read_response
    raw = self._buffer.readline()
          ^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/username/.local/share/virtualenvs/ai-gallery-nT0ArqSL/lib/python3.12/site-packages/redis/_parsers/socket.py", line 115, in readline
    self._read_from_socket()
  File "/Users/username/.local/share/virtualenvs/ai-gallery-nT0ArqSL/lib/python3.12/site-packages/redis/_parsers/socket.py", line 65, in _read_from_socket
    data = self._sock.recv(socket_read_size)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/username/.local/share/virtualenvs/ai-gallery-nT0ArqSL/lib/python3.12/site-packages/gunicorn/workers/base.py", line 198, in handle_abort
    sys.exit(1)
SystemExit: 1

2026年2月8日日曜日

VSCode で Powershell をフォーマットする方法

VSCode で Powershell をフォーマットする方法

概要

VSCode を使うのが一番簡単のようです

環境

  • Windows11
  • VSCode 1.108.2

フォーマット

  • ps1 ファイルを開く
  • 右クリックから「Format Document」

リント

lint は CLI でできます

Invoke-ScriptAnalyzer -Path .\test.ps1

最後に

全部 CLI でやりたい

参考サイト

2026年2月7日土曜日

Slack で管理者権限のあるユーザトークンを取得する方法

Slack で管理者権限のあるユーザトークンを取得する方法

概要

ボットトークン (xoxb-) ではなくユーザトークン (xoxp-) を取得する方法を紹介します
ただし無料プランだと管理者権限のあるユーザトークンは設定できないので注意してください

環境

  • Slack 2026/02/04 時点

アプリ作成

権限設定

  • OAuth & Permissions
  • User Token Scopes
  • admin を追加

アプリ再インストール

  • OAuth & Permissions
  • OAuth Tokens
  • Reinstall to xxx

トークン確認

  • xoxp- から始まるトークンが発行されていることを確認します
  • すでにボットトークンがある場合はその上に User OAuth Token が発行されます

最後に

あとはトークンを使うだけ

参考サイト

2026年2月6日金曜日

GCP のクラウドストレージに上書きアップロードしてもオブジェクトが変更されない場合の対応方法

GCP のクラウドストレージに上書きアップロードしてもオブジェクトが変更されない場合の対応方法

概要

おそらくキャッシュされているのが問題です
特に発生しやすいのが「同じような画像を上書きアップロードした場合」で例えば元の画像を90度回転させた画像などは上書きアップロードしても反映されないことが多いです

そんな場合の反映方法を紹介します

環境

  • GCP 2026/02/04 時点

ブラウザのキャッシュを削除する

とりあえず削除して再度アクセスしてみましょう

クエリ付きでアクセスする

クラウドストレージ側でキャッシュしている可能性があるのでクエリ付きでアクセスしましょう

https://storage.googleapis.com/your_bucket_name/file.jpg?nocache=1

Cache-Control を確認し短くする

curl -I https://storage.googleapis.com/your_bucket_name/file.jpg

これで「Cache-Control:」「Age:」「X-Cache:」があればクラウドストレージ側のキャッシュが有効になっています

  • gsutil setmeta -h "Cache-Control:public,max-age=60" gs://your_bucket_name/file.jpg

60秒など短い時間にしましょう
0 だと毎回クラウドストレージにアクセスしネットワーク量が多くなり金額上がるので注意しましょう

別のファイル名にする

これが一番カンタンです
がアプリ側の改修などが必要になるケースがあるので注意しましょう

よくあるパターンはファイル名にハッシュなどを含めるケースです

ダメだったケース

  • 削除 -> 再度アップロード
    • なぜか元の画像がクラウドストレージ側でも利用されてしまう
  • 実はファイルが回転できていない
    • exif の Orientation にまだデータが残っている
    • ビューアの見た目だけが回転している

最後に

まずはキャッシュを疑いましょう

2026年2月5日木曜日

VSCode の git で認証ダイアログが定期的に出てくる場合の対処方法

VSCode の git で認証ダイアログが定期的に出てくる場合の対処方法

概要

タイトルの通りなぜか定期的に出てくるので抑制します

環境

  • Windows 11
  • VScode 1.108.2

方法

  • git config --global credential.helper cache

最後に

ただしこれでも push や pull 時には認証情報が聞かれるので注意してください
また Windows を再起動しても再度入力する必要があります

wincred とか使ってもなぜか毎回聞かれてしまいます

https の認証だとそうなるのかも

2026年2月4日水曜日

Windows 起動時に特定のページで Chrome を起動する方法

Windows 起動時に特定のページで Chrome を起動する方法

概要

ホームページにはしたくないが Windows 起動時に特定ページを開く方法を紹介します

環境

  • Windows11
  • Chrome 144.0.7559.110

shell:startup を開く

エクスプローラで「shell:startup」と入力し起動時に自動で開くフォルダを開きます

ショートカットの作成

  • 右クリック
  • 新規作成
  • ショートカット

ショートカットの設定

  • 項目の場所を入力してくださいに以下を入力
    • "C:\Program Files\Google\Chrome\Application\chrome.exe" "http://xxx.xxx.xxx.xxx"
  • 次へ
  • 適当なショートカットの名前を設定

最後に

ホームページを設定し Chrome のショートカットをスタートアップに追加してもいいですがホームページに設定すると毎回そのページからになるのでそれが嫌な場合は便利です

2026年2月3日火曜日

VSCode + Continue でエージェント開発をする

VSCode + Continue でエージェント開発をする

概要

前回 VSCode + copilot + ollama でローカル開発する方法を紹介しました
Ask モードしか使えないのが残念でした
今回は VSCode + continue + ollama で Agent 開発してみます

環境

  • M2 mac mini macOS 26.2
    • ollama 0.14.3
    • codegemma 7b
    • llama3.1 8b
  • VSCode 1.108.2

Continue のインストール

VSCode の拡張一覧から「continue」で検索しインストールすれば OK です

https://github.com/continuedev/continue

Continue の設定

  • 左メニューから Continue のアイコンを選択
  • 右上の歯車マークを選択
  • Configs
  • Local Config の歯車マークを選択
  • YAML を以下のように編集 (IP部分は適宜変更)
name: Local Config
version: 1.0.0
schema: v1
models:
  - name: codegemma
    provider: ollama
    model: codegemma:latest
    apiBase: http://192.168.1.65:11434
    roles:
      - chat
      - edit
      - apply
      - autocomplete

動作確認

あとは Continue のチャット画面に移動しチャットすれば OK です
「Agent」になっておりかつ先程設定したモデルが選択されていることを確認してからチャットしましょう

M2 mac mini 上では特にストレスなくレスポンスが返ってきました

トラブルシューティング: Agent モードが使えない

「Agent might not work well with this model」という警告が出ている場合は Agent モードが使えません

codegemma は tools に対応しているモデルではないようです
https://ollama.com/library/codegemma に「Tools」というタグが付与されていないと Agent モードは使えません

なのでモデルを変更しましょう
ただ Tools が使えるモデルは比較的大きめなのでマシンスペックが必要になるので注意しましょう

公式のおすすめモデルは https://docs.continue.dev/ide-extensions/agent/model-setup です

  • ollama run llama3.2
name: Local Config
version: 1.0.0
schema: v1
models:
  - name: llama3.2
    provider: ollama
    model: llama3.2:latest
    apiBase: http://192.168.1.65:11434
    roles:
      - chat
      - edit
      - apply
      - autocomplete
    capabilities: 
     - tool_use
     - image_input

最後に

コンテキストの渡し方 (現在選択中のファイルや別のファイル) を送信する方法などは copilot chat プラグインとは少し違うので注意しましょう

また ollama を使う場合エージェントモードが使えるモデルと使えないモデルがあり使えるモデルはそれなりの大きさのモデルが必要になるようです

gemma3 などは使えませんでした
この辺り最適なモデルの選択などは続けていきたいです

エージェントモードで動かすと回答の処理や Continue の挙動がおかしくなるのでその場合は素直に Chat モードを使いましょう

参考サイト