Enable the Hardware Acceleration of Chrome Browser on AWS Batch

In today’s rapidly evolving tech landscape, optimizing performance is a critical factor, especially when dealing with large-scale processing on cloud platforms. Many of you might find yourselves needing to automate Chrome on a server for various reasons. Additionally, you might want to navigate OpenGL-intensive websites using GPU acceleration. I was one of those people. However, if your tech stack isn’t perfectly aligned, this can be a daunting task.

In my quest to solve this issue, I scoured various QA sites, but I noticed that many users facing similar problems never received a conclusive answer. Therefore, I’ve written this article to address the gap and provide a comprehensive guide to enable hardware acceleration for Chrome on AWS Batch.

Steps to Enable Hardware Acceleration

Create a Docker Image with Chrome and Nvidia Support

First, you’ll need a Docker image that includes Google Chrome and the necessary Nvidia drivers. Here is a basic Dockerfile to get you started:

FROM --platform=linux/amd64 nvidia/cuda:12.5.0-devel-ubuntu22.04

WORKDIR /app

ENV DEBIAN_FRONTEND=noninteractive
ENV TZ=Asia/Tokyo

# For timezone
RUN apt-get update && \
    apt-get install -y software-properties-common tzdata && \
    ln -fs /usr/share/zoneinfo/Asia/Tokyo /etc/localtime && \
    dpkg-reconfigure --frontend noninteractive tzdata && \
    apt-get clean

# ldd chrome | grep found
#    libnss3.so => not found
#    libnssutil3.so => not found
#    libsmime3.so => not found
#    libnspr4.so => not found
#    libatk-1.0.so.0 => not found
#    libatk-bridge-2.0.so.0 => not found
#    libcups.so.2 => not found
#    libdrm.so.2 => not found
#    libxcb.so.1 => not found
#    libxkbcommon.so.0 => not found
#    libatspi.so.0 => not found
#    libX11.so.6 => not found
#    libXcomposite.so.1 => not found
#    libXdamage.so.1 => not found
#    libXext.so.6 => not found
#    libXfixes.so.3 => not found
#    libXrandr.so.2 => not found
#    libgbm.so.1 => not found
#    libpango-1.0.so.0 => not found
#    libcairo.so.2 => not found
#    libasound.so.2 => not found

RUN apt-get update && apt-get install -y \
    libnss3 \
    libnss3-tools \
    libnspr4 \
    libatk1.0-0 \
    libatk-bridge2.0-0 \
    libcups2 \
    libdrm2 \
    libxcb1 \
    libxkbcommon0 \
    libatspi2.0-0 \
    libx11-6 \
    libxcomposite1 \
    libxdamage1 \
    libxext6 \
    libxfixes3 \
    libxrandr2 \
    libgbm1 \
    libpango-1.0-0 \
    libcairo2 \
    libasound2 && \
    apt-get clean

# [ERROR:egl_util.cc(44)] : Failed to load GLES library: libGLESv2.so.2: libGLESv2.so.2: cannot open shared object file: No such file or directory
# [ERROR:egl_util.cc(52)] : Failed to load EGL library: libEGL.so.1: libEGL.so.1: cannot open shared object file: No such file or directory
RUN apt-get update && apt-get install -y libgles2-mesa libegl1-mesa && apt-get clean

# [ERROR:gl_display.cc(520)] : EGL Driver message (Critical) eglInitialize: xcb_connect failed
RUN apt-get update && apt-get install -y xvfb && apt-get clean

# xvfb-run --server-args="-screen 0 1920x1080x24 +extension GLX +render -noreset" \
# chrome \
# --enable-logging --v=1
# --headless \
# --ignore-gpu-blocklist \
# --enable-gpu-rasterization \
# --enable-zero-copy \
# --use-angle=default \
# --no-sandbox
RUN apt-get update && apt-get install -y \
    libva2 \
    libva-x11-2 \
    libva-drm2 && \
    apt-get clean

To get started, you’ll need a Docker image that includes the necessary Nvidia drivers. This Dockerfile will ensure Chrome runs smoothly. In my setup, I used Python’s Pyppeteer, which downloads Chrome, so there’s no need to install Chrome via apt-get. Of course, Puppeteer also works, and if you prefer, you can directly install Chrome using apt-get.

Pay close attention to the comments within the Dockerfile. The comments explain the commands executed, the errors encountered, and the libraries installed to address those errors. As of July 2024, this Dockerfile is functional. However, if you encounter any errors in the future, refer to these comments to guide your troubleshooting and adjustments.

Prepare Your Project

In this step, we will show an example using Pyppeteer in Python. If you prefer using Puppeteer, feel free to substitute Python with Node.js as appropriate.

There are a few key points to keep in mind, but one of the most important is to run your Python or Node.js through xvfb-run. This is crucial not only to allow Chrome to operate in a headless mode but also to enable GPU usage.

Here is an overview of what you need to do:

1. Prepare a shell script for running Python or Node.js on X

xvfb-run --server-args="-screen 0 1920x1080x24 +extension GLX +render -noreset" \
  python3.12 /app/run.py "$@"

2. Run Chrome in a Python Script

import asyncio
import logging
from typing import TypeAlias
from pyppeteer.launcher import launch

_BrowserLaunchOptions: TypeAlias = dict[str, list[str] | bool]

def _get_launch_options() -> _BrowserLaunchOptions:
    args = [
        "--lang=ja-JP",
        "--no-sandbox",
        "--ignore-gpu-blocklist",
        "--enable-gpu-rasterization",
        "--enable-zero-copy",
        "--disable-gpu-process-crash-limit",
        "--use-angle=default",
    ]

    if logger.level == logging.DEBUG:
        args.extend(["--enable-logging", "--v=1"])

    launch_options: _BrowserLaunchOptions = {"args": args, "ignoreHTTPSErrors": True}

    if logger.level == logging.DEBUG:
        launch_options["dumpio"] = True

    logger.debug(f"launch_options={launch_options}")
    return launch_options


async def main() -> None:
    launch_options = _get_launch_options()
    browser = await launch(**launch_options)
    # do something you need
    await browser.close()
 

if __name__ == "__main__":
    asyncio.get_event_loop().run_until_complete(main())

This is a code snippet. In this code, we only create the browser object, but you will need to further automate Chrome operations as per your requirements. The key point here is the launch options. If you get these options wrong, Chrome will not start. Pay particular attention to the arguments passed to args. In DEBUG mode, configure Chrome to output more logs. This way, if any issues arise, you can usually resolve them. If you cannot read the logs yourself, you can always rely on ChatGPT for assistance.

If you want to quickly verify that everything is working correctly, you can navigate to chrome://gpu, capture a screenshot of the page, and upload the screenshot file to S3. This will help you confirm that GPU acceleration is enabled.

page = await browser.newPage()
await page.goto("chrome://gpu")
await page.screenshot({"path": "./screenshot.png"})

As a side note, there is always a possibility of encountering errors in every operation, such as opening a tab, navigating to a URL, or retrieving content. Therefore, I write my code to restart from the point of opening a tab if any single operation fails. This process is repeated until it succeeds, but after five failures, it gives up. When automating Chrome, you need to write your code defensively to this extent.

3. Update the Dockerfile to adjust the Python Project

Please add the following content to the Dockerfile we prepared earlier. I will omit the details on how to create the requirements.txt.

# Install python3.12
RUN add-apt-repository ppa:deadsnakes/ppa -y && \
    apt-get update && \
    apt-get install -y python3.12 python3.12-distutils && \
    apt-get install -y wget && \
    apt-get clean

# Install pip
RUN wget https://bootstrap.pypa.io/get-pip.py && \
    python3.12 ./get-pip.py

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

Set Up an Environment on AWS Batch for GPU-Accelerated Chrome

In this final step, we will set up an environment on AWS Batch that can utilize GPU-accelerated Chrome. For this example, we will use the g4dn instance type, which is optimized for GPU workloads.

Here is a snippet of the AWS CDK code to set up the necessary infrastructure:

export class BatchChromeStack extends cdk.Stack {
  constructor(scope: Construct, id: string, stage: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const vpc: ec2.IVpc = this.getVpc();
    const subnets = this.getSubnets(vpc);
    const securityGroup = this.getSecurityGroup(vpc);

    const instanceRole = this.createInstanceRoleAndProfile();

    const { repository, image_name } = this.createAndDeployDockerImage();

    const jobRole = this.createJobRole();
    this.addSSMPolicyToJobRole(jobRole, stage);

    const bucket = this.createS3Bucket();
    this.addS3PutObjectPolicyToRole(jobRole, bucket);

    const container = this.createContainer(image_name, jobRole, bucket, stage);

    repository.grantPull(container.executionRole);

    this.createEcsJobDefinition(container);
    const computeEnvironment = this.createComputeEnvironment(vpc, [securityGroup], subnets, instanceRole);
    this.createJobQueue(computeEnvironment);
  }

  // Omit many codes

  private createContainer(image_name: string, jobRole: iam.Role, bucket: s3.Bucket, stage: string): batch.EcsEc2ContainerDefinition {
    const logLevel = stage === 'production' ? 'INFO' : 'DEBUG';

    return new batch.EcsEc2ContainerDefinition(this, `${this.NAME_PREFIX}ContainerDefinition`, {
      image: ecs.ContainerImage.fromRegistry(image_name),
      // g4dn.xlarge
      gpu: 1,
      cpu: 4,
      memory: cdk.Size.gibibytes(15), // 16GB - 1GB for OS
      jobRole: jobRole,
      logging: new ecs.AwsLogDriver({ streamPrefix: this.KEBAB_NAME_PREFIX }),
      environment: {
        BUCKET_NAME: bucket.bucketName,
        LOG_LEVEL: logLevel,
      },
    });
  }
}

I have omitted quite a lot of details in this step. I did not include what you can search online or ask ChatGPT to get answers, as those are readily accessible resources.

The biggest issue I faced was that when I specified the memory size of 16GB, which is available on the g4dn.xlarge instance, in the EcsEc2ContainerDefinition, the instance failed to launch. However, it did launch with 15GB. I believe this is a quirk of AWS Batch.

If you need the full version of the CDK code or if something is not working correctly, feel free to reach out. If I feel inclined, I might provide more detailed information.

舌癌になったので、頭の中を書き出してみた

なぜ、この記事を書くのか?

約9年ぶりの投稿です。9年前、情報公開が難しい会社(とあるFinTech企業)から情報公開を価値と認めてくれる会社(gumi)へ転職したため、所属企業の名前で記事を書いたり、イベントに登壇したりしていました。結果、この個人ブログに書くネタがなくなり放置していました。そして二年前、WOVN という会社に転職し、Developer から Product Owner (CSM-SM 持ってるのに Scrum Master じゃないのかというツッコミ歓迎) に役割を変え、さらに書くネタがなくなりました。

ところが、2022年9月に舌癌が発覚し、大きく私の死生観が変わったため、個人ブログにまとめてみようかと思います。私自身が私以外のがんサバイバーの方々の明るい日記に心を救われため、この記事も、少しでも、同じ境遇の誰かの励みや助けになれば幸いに思います。

伝えたいことのまとめ

ほとんどの方に言えること
  • 40過ぎたら、がん検診を絶対受けて!
  • 日本の大学病院の標準治療を信じよう!
  • 癌と共に歩もう!
  • 運動、睡眠、食事、笑顔、コミュニケーション力と真剣に向き合おう!
人によって違うこと
  • 人によるけど、舌の生検、そんなに痛くないよ
  • 人によるけど、全身麻酔、怖くなかったよ
  • 人によるけど、舌の部分切除の術後は、痛み止め不要だったよ
娘にドヤったこと
  • 自分が死ぬことを真に自覚するまで、人は何歳になっても子供なのだよ( ï½°`дー´)

経緯

2022年6月

何となく10年前に虫歯を治療済みの歯がしみるような気がする。(奥から二番目の奥歯)

しかも、同じ側の耳の奥も痛いし、頭痛もする。

2022年8月

歯の症状は治っていたが、耳の奥が痛いため、耳鼻科と歯医者さんの選択に迷い、とりあえず虫歯だったら嫌だなーと思い歯医者さんへ。しかし、該当の部位に虫歯は見つからず。歯石掃除、歯磨き指導、ベロ磨き指導を受ける。帰宅後、ベロ磨き中、舌の側面に 2mm くらいの白い口内炎のような出来物を見つける。(9月に白板症と分かる) とりあえず、ベロ磨きで擦っても痛くないため放置する。

2022年9月

耳の奥の痛み、頭痛が気になり、耳鼻科へ行こうかと思ったが、妻の勧めで自宅の近所のかかりつけの内科へ。そこの先生は、過去、他の病院でも分からなかった私の病気(既に快癒してる)を見抜いて治療してくれた実績があり、何人かのお医者さんを見てきた看護師の妻から見ても名医とのこと。

そして、かかりつけ医に診てもらったところ、これは舌癌の可能性があるから、大学病院への紹介状書くねとなった。虫歯くらいの気持ちでいたところに生死に関わる「癌」と聞いてしまい、ショックを受けた私は混乱しながら、とりあえず、仕事の予定を見ながら一週間後に大学病院の検査の予定を入れてもらった。

後に妻から聞いた話によると、先生的には、堀ちえみさんの件があったから、疑わしきは、早めに検査するべきだと思っただけで、癌確定とは思っていなかったらしい。とはいえ、ショックを受けた私は翌日から夜に寝れなくなり、昼は微熱と悪寒に悩まされ、耳痛も頭痛も酷くなった。そして、2日後、再び妻に連れられて、かかりつけ医のところへ行き、検査予約を一週間後ではなく、数日後の予定に早めてもらい、睡眠導入剤を処方してもらった。また、念のため血液検査をしてもらった。結論、悪寒や頭痛は心気症だった。(これは後に調べたことだか、癌は末期まで痛みがない)

その後、恐る恐る大学病院へ行き、細胞診というブラシで病変を擦って調べる検査を受けた。結果が出るまで生殺し状態で、不眠は続いた。この頃、死に備えて何をすべきかと考えていた。

ちなみに担当医の初見では、この小ささだと、耳や頭の痛みは全く関係ないと思われるとのこと。これは、あとから気がついたことだけど、人は何でも癌や標準治療の副作用のせいにしてしまうが、それに本人が気がつくのは本当に難しい。私は癌自身は痛みを伴わないと知ってから腹落ちした。

2022年10月

大学病院で細胞診の結果を聞いた。主治医が「結果は3です。」と伝えてきたため、あ、ステージ3なのか、人生が終わったなーと考え「そうですか…」と答えたら主治医から「いや、クラス3で安心しないでください」と注意で返された。ん?何か話が噛み合ってない。安心したんじゃなくて、運命を受け入れたんだよ?あれ…?よくよく話を聞くと、検査結果は五段階のクラスで表現されるらしく、大雑把に言うとクラス1-2は良性、3は不明、4-5悪性。だから、もっと詳しく調べるために、次は生検しましょうとのこと。患部を部分麻酔して切り取って病理検査し、より詳しく判断するのだとか。また、不調の理由が不明な期間が続くのかと不安になる。

ちょっと舌癌の生検についてネットで検索すると、痛みが強いとか、夜寝れないとか、一週間は痛いとか、散々な情報しか見つからず凹む。主治医からは、人によって異なるが、大凡、大した事ないと感じる人が多いと伝えられる。

そして、10月末に生検を受ける。麻酔の針は4本刺されたが、まー、歯医者で慣れてるから、それほど痛く感じない。その後は痛みを何も感じなかった。切っている最中は電動メスで止血していたらしい。最後に四針縫って生検終了。出血確認後に解散。妻の勧めで、すぐに薬局でロキソニンをもらって飲んだ。当日、麻酔が切れてから疼くような痛みを感じたが、食事以外で強い痛みを感じることはなく、翌日から痛み止めを飲むのは止めた。食事中、ちょっと気を抜くと、余った糸を噛んで悲鳴を上げていたが、三日目からラーメンを食べられるくらいに回復していた。生検の結果が出るまで、最大で二週間かかるらしく、またもや、モヤモヤ期間が続くことに凹む。とはいえ、生検のあと、耳と頭の痛みが消えた。近所のかかりつけ医の話によると、舌の神経は、いろんなところに繋がっているからねーとのこと。

2022年11月

11/7 に生検の結果、T1M0N0 の癌(高度異形成)と結果を聞く。垂直方向は生検で取り切ったが水平方向には残っているとのこと。放置した場合、広がる可能性ある。このサイズだと、抗がん剤も放射線もデメリットが大きいため、すぐに手術で切除が良いとのこと。安全マージンをとって大きく切除するが、味覚も会話も術後と変わらないと説明を受け、切除で決心する。手術中、迅速病理診断を行い、もし、癌の取り残しがあれば、追加で切除するという話もあり、とても怯えた。また、術中に神経を触るか切断すると、舌先に痺れが長期間出るとのこと。怖すぎる。一番早い手術の予約でも 12/2 とのことで、三週間待ち。遅い…と思いつつ、その日で予約したのたが、実は、この三週間が、後から振り返ると、とても良い準備期間となった。

私の仕事は人と話すことだし、趣味は料理なので、言葉と味覚が温存されるだけ、ありがたく思おう…。

この癌のサイズだと、基本的には大丈夫だけど、念のため、鎖骨から上の CT も撮りましょうとなり、そちらも翌週に予約。CT 自体は、造影剤を点滴でいれる必要があり、しかも、ちょっと太い針を刺すが、そんなに負担ではなかった。撮影中、ずっと痛かったけどね…。そして、結果、リンパへの転移が疑われる微妙な結果が出てしまう。放射線技師が、これは怪しいと言っており、主治医は、こんな小さいの関係ないけど…うーん、経過観察しましょうとのこと。これが転移だとステージ3になるし、リンパ切除の追加手術になるとのこと。癌がリンパに転移すると、大雑把に言うと免疫力が落ちるため、非常にまずい…。しかし、顎や首のリンパを切除すると後遺症の克服が大変そう。うむむ。

2022年11月 (準備期間)

癌と確定したので、癌とは何かここで真剣に調べた。その結果、私の死生観も大きく変化した。私はいつも何か調べる際、速効性のある簡単な行動を示しているものは無視する。(私が作る文章やプレゼン資料は、その系統なのにw) 重要なのはマインドセットとエビデンスレベル。結果、次の考えに至る。

  • 癌は寛解するが完治は難しい
  • 癌と共に生きる(本来の寿命に近づける)
  • 癌は末期まで痛みを伴わない(だから初期で見つけるのは難しい)
  • 痛みを伴うのは癌の治療行為
  • 治療行為に耐えられる身体と心を作る
  • 癌と身体の不健康は直結していない (癌でも健康はあり得る)
  • 心まで病にする必要はない
  • 日本の大学病院の癌医療は世界的にみて高い水準
  • 標準治療はすごい
  • 標準治療の「補助」として、他の医療行為はある (標準治療の副作用を軽減したり効果を後押しする)
  • 他の医療行為は詐欺が多すぎる (見抜き方はあるが…絶対ではないから書かない)
  • 免疫力を測る方法はない(その提案は多くある)
  • とはいえ、40歳から免疫力は格段に落ちる
  • 情報、睡眠、運動、食事、笑顔、人とのコミュニケーションを「真剣」に大切にする

ちなみに舌癌については次の通り。

  • 昔は、刺激物(酒、タバコ、辛いもの)の摂取が好きな人たちに多かった
  • 今は、顎の狭い人たちに多い(歯が舌に当たり続けるため)
  • 30代なら歯を矯正する
  • 40代なら歯を少し削って丸めるか、マウスピースを付ける

私は45歳なので、大学病院の主治医の先生からの提案でプラスチックのマウスピースにした。これは術後の舌を切除した痛みも軽減することに術後に気がついた。私は、もともと歯を食いしばる癖があり、舌も頬も噛み跡だらけだったので、もうマウスピースから離れられないくらい依存している。

それから、とりあえず、約二週間半ほど使って身体を準備することにした。というか、そのくらいの悪足掻きしか出来なかったとも言える。ここで行ったことに意味があったのかは、全く分からないが、心の安定には繋がったし、体脂肪計の測定結果が劇的に変わった。

  • 毎朝起きたら水だけ飲んで、10分間のインターバルトレーニング (アクトレブログ参照のこと)
  • 日に高負荷の自重スクワット3セット (1セットで歩くのが困難になるため、間隔は開けてる)
  • 野菜中心の食事と毎日45g の納豆を食べる (筋肉量のために肉も米も食べる)
  • 寝るために、18:00食事、20:00風呂(40度でBARTHに20分)、22:00柔軟(オガトレ参照のこと)
  • 妻と過ごす (家でドラマみたり、映画館行ったり、外食したり、手を繋いで寝てもらったりw)

BARTH 高いよね…。でも睡眠のために課金した。

2022年12月

12/1 入院し、12/2 全身麻酔にて切除。そして、この記事を書いているのが 12/3 (今日)。特に 12/3 はブログを 9 年振りに更新しようと思い立つくらい暇だった。

12/1 妻に付き添われて入院手続きを行う。病棟のベットに案内されたあと、荷物を置いて妻と共に案内のコンビニへ買い出しへ。病院に持ち込んだのは、下着、スマフォ、スマフォ充電用のケーブル、マスクのみで、他の必要なものは、レンタルと現地調達が可能。しかも、コンビニは電子マネー、レンタルは退院時の後払いが可能なので、スマフォがあれば財布も不要。

コンビニで購入したものは次の通り。

  • 手術時に着用するT字体(ふんどし)
  • 食器用洗剤とスポンジ
  • いろはす

レンタル手続きしたものは次の通り。

  • パジャマ
  • バスタオルとフェイスタオル
  • 歯ブラシ、歯磨き粉、コップ、シャンプー、リンス、ボディーソープ (レンタル屋だけど買い切り)

その後、妻と院内のカフェでコーヒーを飲んで、雑談して解散。コロナ禍で一度病棟に戻ると面会できないため、術前は会いに来なくて良いと伝える。ただ、術後すぐには、会いたいねーとだけ伝えて解散。その日のお昼からは暇かと思っていたら、麻酔科医、歯科医、口腔外科医の問診や、手術前の歯石取りなどがあり、そんなに暇ではなかった。お昼と夜は、病棟のご飯を食べたが、とても美味かった。生検の傷が癒えてから、食べるもの全てが美味く感じるから、そのせいだと思う。お昼ご飯の後には、気持ちを落ち着かせるためにスクワットをしまくった。もはや、スクワットは精神安定剤。

夜、シャワーを浴び、指示通りに足の裏に油性ペンで名前(漢字フルネーム)を書き入れ、ベットに入る。しかし、ベットが固く背中や腰が痛くなり、また、翌日の手術の迅速病理診断の結果や、術後の痛みが怖くなってしまい、結局、1時間置きに目が覚めてしまった。

12/2 朝一の手術で、かつ、術後はしばらくシャワーを浴びれないとの話だったので、7:00 に病室に備え付けのシャワーを浴びて落ち着きを取り戻す。8:00 にふんどしを締め、オペ着を見にまとい、昔、開発に携わったソシャゲのデイリーミッションを消化し、ギルメンに挨拶してスマフォを鍵付きの箱に入れて、お迎えを待った。その後、お迎えの看護師さんが来たので、いざオペ室へ。オペ室へ移動していると、オペ予定の患者の方がどんどん合流してきて、こんなに大勢が朝から手術を受けるのか…と圧倒される。

その後、名前を呼ばれ、何点か確認してオペ室へ移動。移動中、様々な機器が大量に置かれているのが興味深く、写真を撮りたいなーとキョロキョロ見回していたらすぐにオペ室に到着。執刀の主治医が見当たらず、心細くなるが、麻酔科医と挨拶して指示に従う。点滴の針を入れますよーと言われたあと、何度もブスブス刺され、痛いっすと言ったら、別の麻酔科医が針を入れてくれた。ちょっと、こんなんで大丈夫?と思っていたら、酸素マスクをつけられ、「まずは酸素ですよー」と言われた後の記憶がない…。

気がついた時、主治医の先生が私の乗ったベットを押しながら「もう終わりましたよー。10:30 ですよー。」と話しかけてきて覚醒した。先生の顔を見れて安心しましたーと答えた記憶がうっすらあるw

そこから待機室で30分待つことに。吐き気があれば教えて欲しいとのことで、吐きそうと伝えたが、大きなゲップが出ただけでスッキリしてしまった。とは言え、吐き気止めの薬を点滴に入れてもらう。

30分後、病棟の看護師に身柄を引き継がれ、病室へ移動。移動中、病棟の看護師さんに、背が高いですねーとか雑談を振られたので、頑張って受け答えしてみたが、記憶が曖昧で何と答えたか覚えていない。途中、看護士さんが、実は奥さんが来ているので、病棟の入り口で少しだけ会えますと教えてくれた。妻には術後に会って無事を直接伝えたかったので、会えて本当に良かった。しかし、私は妻に何を伝えたのか、意識朦朧としていて、あまり記憶に残っていない。とは言え妻が曰く「舌の手術したはずなのに、はっきり話してたよ」とのこと。

その後、三時間、枕無しで酸素吸入の儀がスタート。寝てやり過ごそうと思ったが寝れず、スマフォを触って何とかやり過ごす。酸素マスクが外れた後、ほぼ半裸の貧ぼっちゃまスタイルからパジャマに着替え、点滴から抗生物質を入れてもらいつつ安静にすることに。とりあえずスマフォで方々へ「手術終わって覚醒したよー」の連絡を入れまくる。

しばらくすると、主治医の先生が挨拶に来てくれて、一回の切除で終わり、迅速病理診断は上下左右奥の5方向に問題なく、妻にそれらを説明したことなど教えてもらった。また、後で回診で大勢で来るけど、寝てて良いからねーとのこと。そして、今日から夕ご飯食べて良いから、病棟に伝えておくねーとのこと。え…食べれるの、怖すぎる…と思ったが、早く退院したいから挑戦することに。結果、完食できた。痛み止めの投与がされていないのに痛くないぞ…。

その夜、病院のベットが硬い理由を調べ、角度を変えるなどの工夫で、腰や背中の痛みが軽減されることを知る。私的には寝る時に14度、起きてる時は46度が良かった。また、点滴をしているとトイレが近くなるため頻繁にトイレに行くのだが、その度に血が逆流する。これも、心臓より針の刺さってる箇所を低くすると解消しやすいことをソシャゲ経由でギルメンに教えてもらい解決。

12/3 硬いベットと点滴のせいで、あまりよく寝れず。かつ、6:00 からしゃっくりが止まらなくなる。ご飯を食べて胃が温まるとしゃっくりは止まり、冷たいものを飲むとしゃっくりが始まることに気がつく。軽く調べてみると全身麻酔の一時的な後遺症らしい。とりあえず、8:00頃に看護師さんに、お風呂に入って着替えたい旨を伝えると、実は外来から呼ばれているので、先にそっちに行ってーとのこと。仕方ないので点滴を引きずって外来へ行くと、途中で主治医の先生と合流。ちょっと移動を手伝ってもらいつつ受付で待っててねーと分かれる。その後、診察室で診察を受け、この治り具合なら 12/5 に退院して良く、点滴もすぐに外すことに。都合悪ければ、もっと入院できるけど…となり、いえいえ、絶対帰ります!と答えた。その後、病室で看護師に点滴をとってもらい、念願のシャワーとお着替え。麻酔科医に痛み止め一回も処方されませんでしたね…と言われ、あんなに痛みを恐れていたのは何だったのかと、自分自身に呆れる。多分、主治医の腕が良いからかもしれないし、下準備で筋トレしておいたのが効いたのかもしれない。お昼ご飯を食べてしゃっくりが止まったので、スクワットをして一休みしてから、院内の庭園に遊びに行き、外の冷たい空気を吸ったらしゃっくり再発。コンビニに寄ってお茶を購入し、電子レンジで温めて暖を取ることに。

続きに関して

時間軸が今に追いついたので、一旦、ここで筆を置きます。これから、退院した後も転移に備えて二週間に一回ペースでリンパ節の様子を見て行く必要がありますし、切った部位の病理検査の結果を確認する必要もあります。最低五年は病院へ通うことになるから、気が向いたら、続きを書こうと思います。

最後に

家族や妻の親族、友人、同僚に心配をかけて本当に申し訳ないと思いつつも、様々な方法でサポートをいただき本当に感謝しています。必ず受けた恩は返します。特に妻には、本当に感謝しており、もう何を返して良いのか想像もつかないけど、一生をかけて何か幸せ的なものを返して行きます。

RabbitMQ 3.x から Queue のミラーリング方法が変更になった

今まで, Queue 作成時に指定していたミラーリングのパラメータは廃止になり, rabbitmqctl コマンドで指定するように変更なった.
Perl の AnyEvent::RabbitMQ や Python の kombu(celery) で x-ha-policy を指定する方法は, RabbitMQ 2.x までしか通用しない.*1

経緯や詳細は, 下記二つを参照の事.

指定方法

Web UI の Manage Console からも指定できるが, 今回は rabbitmqctl の例を挙げる.*2

Usage は下記の通り.

$ rabbitmqctl set_policy <ポリシーの名前> <パターン> <モード> [<優先度>]
ポリシー名 ポリシーを一意にし, 更新や削除の操作対象とする.
パターン キュー名がパターンにマッチした場合, ポリシーの対象とする. 正規表現が使える.
モード RabbitMQ - Highly Available Queues 参照の事.
優先度 数値が高い程, 優先. 未指定で 0 が入る.

指定例

全てのキューをミラーリングする(ものぐさ設定w;).

$ rabbitmqctl set_policy all '^.*' '{"ha-mode": "all"}'

キュー名の接頭辞に ha が付いた場合にミラーリングする.

$ rabbitmqctl set_policy ha-all "^ha\." '{"ha-mode":"all"}'

キュー名の接頭辞に amq が付いた場合(キュー名を指定せず, RabbitMQ に自動採番させた場合) "以外" にミラーリングする.

$ rabbitmqctl set_policy ha-ignore-amq '^(?!amq\.).*' '{"ha-mode": "all"}'

運用中にポリシーの変更が可能らしいが, 検証はこれから行う予定.
運用中にポリシーの変更は可能. また, クラスタリング時の Manage Console のポリシー表記が変だったので, 下記のコマンドで動作確認した方が良い.

$ rabbitmqctl list_queues name policy pid owner_pid slave_pids messages_ready messages_unacknowledged messages

尚, ポリシーは, パターンにマッチする Exchange にも適用されている. これが, どのような影響を及ぼすのかは調査中.

*1:3.x で指定してもエラーにならないので, 問題に気がつき難い

*2:私は, Manage Console で操作するのが良いと思う

Python の amqplib とか py-amqp で Message を Consume する際, ヘッダに x-death が付与されていると落ちる件

データに謎の 'A' という型が定義されているのが問題. AMQP の Elementary domains を見たのだけれど 'A' が何か見当たらない…。助けて偉い人orz
とりあえず, amqplib 1.0.2 は, 以下の黒魔術で回避可能. kombu 2.5.0 から amqplib に代わり py-amqp がデフォルトで使用されるのだけれど, そちらはおいおい.

from struct import unpack
from decimal import Decimal
from amqplib.client_0_8.serialization import AMQPReader

def _patched_read_table(self):
    """
    Read an AMQP table, and return as a Python dictionary.
    """
    self.bitcount = self.bits = 0
    tlen = unpack('>I', self.input.read(4))[0]
    table_data = AMQPReader(self.input.read(tlen))
    result = {}
    while table_data.input.tell() < tlen:
        name = table_data.read_shortstr()
        ftype = ord(table_data.input.read(1))

        if ftype == 65: # 'A' これが新しく加わった!!
            len = unpack('>I', table_data.input.read(4))[0]
            ftype = ord(table_data.input.read(1))

        if ftype == 83: # 'S'
            val = table_data.read_longstr()
        elif ftype == 73: # 'I'
            val = unpack('>i', table_data.input.read(4))[0]
        elif ftype == 68: # 'D'
            d = table_data.read_octet()
            n = unpack('>i', table_data.input.read(4))[0]
            val = Decimal(n) / Decimal(10 ** d)
        elif ftype == 84: # 'T'
            val = table_data.read_timestamp()
        elif ftype == 70: # 'F'
            val = table_data.read_table() # recurse
        else:
            raise ValueError('Unknown table item type: %s' % repr(ftype))
        result[name] = val
    return result

AMQPReader.read_table = _patched_read_table

Python Web Framework Advent Calendar 2012 (9日目) Django Model で Named Scope

前置き

この記事は、2012 Pythonアドベントカレンダー(Webフレームワーク) - connpass の 9 日目の記事となります。
今回は、Rails の Named Scope の真似を Django Model で実現する方法と、それを利用した論理削除の紹介を行います。

Django Model で Named Scope を実現する

そもそも何をしたいのか?

まず、ECサイトやソシャゲー等のユーザ情報から、最近登録したユーザの中から直近のアクセス順に上位5人を取得する例を挙げます。

import datetime
from django.utils.timezone import get_default_timezone

# 一週間以内の登録を "最近登録した" とみなす
dt = datetime.datetime.now() - datetime.timedelta(weeks=1)

# utc でも良いけれど、何となくローカライズ
dt = get_default_timezone().localize(dt)

User.objects.filter(created_at__gt=dt).order_by('-logged_in_at')[:5]

Named Scope を使用すると、次のようになります。

User.objects.by_newbie().order_by_active()[:5]
Named Scope を作る

では、実際の実現方法を紹介します。

import datetime
from django.utils.timezone import get_default_timezone

from django.db import models
from django.db.models.query import QuerySet

# Manager と QuerySet で同様のメソッドを使用するので Mix-in Class として切り出す
class UserScopesMixin(object):
    _newbie_term = datetime.timedelta(weeks=1)

    def by_newbie(self):
        dt = datetime.datetime.now() - self._newbie_term
        dt = get_default_timezone().localize(dt)
        return self.filter(created_at__gt=dt)

    def order_by_active(self):
        return self.order_by('-logged_in_at')


# QuerySet に Scope を Mix-in する
# 継承順は賛否分かれる所ですが、この記事では、社内の目があるので Mix-in Class を後ろに羅列します(w;
# 蛇足ですが、私は、私用で Python を書く場合に限り、Mix-in Class を前に羅列する派です
class UserQuerySet(QuerySet, UserScopesMixin):
    pass


# Manager に Scope を Mix-in し, 上記で定義した QuerySet を返すようにする
class UserManager(models.Manager, UserScopesMixin):
    def get_query_set(self):
        return UserQuerySet(self.model)


# 上記で定義した Manager を objects に設定する
class User(models.Model):
    objects = UserManager()

    created_at = models.DateTimeField(auto_now_add=True, index=True)
    logged_in_at = models.DateTimeField(auto_now=True)

    @classmethod
    def get_active_newbies(cls, limit=5):
        return User.objects.by_newbie().order_by_active()[:limit]

結局、get_active_newbies() を定義するのであれば、Named Scope なんて不用ではないか?と思われるかもしれません。
しかし、get_active_newbies() の様なメソッドを多数定義する場合、スッキリ書けるのでオススメです。
また、Named Scope が癖になっていると、そもそも QuerySet をカスタム済みであるため、他のカスタム QuerySet を組み込む際に労力が減るという副作用もあります。*1

論理削除の実例

物理的にレコードを削除せずに、削除フラグを立てて削除した事にするアレ。

class LogicalDeleteScopesMixin(object):
    def by_alive(self):
        return self.filter(deleted_uuid='')

    def delete(self):
        self.update(deleted_uuid=uuid.uuid4(),
                    deleted_at=datetime.datetime.now(pytz.utc))


class LogicalDeleteQuerySet(QuerySet, LogicalDeleteScopesMixin):
    pass


class LogicalDeleteManager(models.Manager, LogicalDeleteScopesMixin):
    def get_query_set(self):
        return LogicalDeleteQuerySet(self.model).by_alive()


# Mix-in Class であるため、object を継承したいが、
# Django Model の制約により models.Model を継承する必要がある。
class LogicalDeleteModelMixin(models.Model):
    class Meta:
        abstract = True


    class RedeletedError(Exception):
        pass


    objects = LogicalDeleteManager()

    # delete_at を有効/無効の確認に利用すると、
    # 一意キー制約を設けた際に、一秒以内の delete が使えないため、
    # 有効/無効を判断するための UUID フィールドを設ける。
    # 初期値に NULL を指定すると、NULL はレコード毎に異なる値と認識されるため、
    # UUID フィールドを一意キー制約に含められない。
    # そこで、初期値には空文字列を明示的に指定しておく。
    deleted_uuid = models.CharField(max_length=255, db_index=True, default='')

    # 念のため、記録として削除日時を残しておく。
    deleted_at = models.DateTimeField(blank=True, null=True)

    def delete(self):
        if self.deleted_uuid:
            raise self.RedeletedError, self.pk

        self.deleted_uuid = uuid.uuid4()
        self.deleted_at = datetime.datetime.now(pytz.utc)
        self.save()

早速、先ほどの User Model で使用してみましょう。

# LogicalDeleteScopesMixin を継承する
class UserScopesMixin(LogicalDeleteScopesMixin):
    pass # 内容に変更がないため省略


# LogicalDeleteScopesMixin を継承して UserScopesMixin を定義したので
# UserQuerySet に変更はない。
class UserQuerySet(QuerySet, UserScopesMixin):
    pass


# by_alive() を使用する必要がある
class UserManager(models.Manager, UserScopesMixin):
    def get_query_set(self):
        return UserQuerySet(self.model).by_alive()


class User(models.Model, LogicalDeleteModelMixin):
    pass # 内容に変更がないため省略

その他

私は、Python 歴 = Django 歴 = 半年未満であり、Django 以外の他の Python Web Framework の知識は皆無という状態ですが、今回紹介させて頂いた Named Scope や、Class Based View の存在から、Django は OO 設計し易いフレームワークだと認識しており、これからも末永くお付き合いできれば嬉しいなぁ〜と考えております。

*1:拙作に MemoizePerRequestQuerySet と MemcacheQuerySet というものがあるのですが、そちらは、Python 系勉強会で紹介予定です

ZeroMQ Erlang Binding(NIF) の inproc と Erlang の素のメッセージ送信の速度を比較してみた

コードと結果は下記の通り
https://gist.github.com/3193117

Erlang で作ったサーバに LL で作ったワーカーをぶら下げようと考えており、どうせならナウでヤングな ZeroMQ を間に入れてみようと思い立ちました。
ズボラな私は、ZeroMQ にワーカーのロードバランスをして欲しかったので、Erlang の中で Queue デバイスを使用し、そこに複数の Erlang プロセスから inproc でメッセージを送信しまくる予定でした。
しかし、ここまで素のメッセージ送信と速度に差があるなら、Erlang の中で自前でロードバランスした方が良さそうかなぁと…思い直してます。

ちなみに、inproc を使ってみて初めて気がついたのですが、inproc は他のトランスポートと下記の点で異なります。

  • bind と connect に使うコンテキストは同じ物でなければいけない
  • connect の前に必ず bind を行う必要がある

100マス計算のシートを生成する

パズル教室にて娘の数学的センスをベタ褒めされたものの、計算速度が遅いので100マス計算を家族でやるようにと指示を受けた。
早速、Python で100マス計算シートを HTML 形式で出力するコマンドを作成したのだが、身近に Haskeller が居るのだから Haskell で書いて添削してもらおうと思い立った。ケーキ一切れで請け負ってくれるだろうか?w;
とりあえず、添削前のコードを晒しておく。添削後には、もっと美しく高機能(例えば HTML で出力する機能が加わったり…)になる予定。

import System.Random
import System.IO
import Data.List

gen_cells :: Integer -> Integer -> IO [String]
gen_cells min max = do
    gen <- newStdGen
    return $ take 10 $ map (\n -> show n) $ randomRs (min, max) gen

i2path :: Integer -> String
i2path i = "./" ++ show i ++ ".txt"

print_table :: String -> String -> [String] -> [String] -> IO ()
print_table path method x_cells y_cells = do
    let head = concat $ intersperse " " x_cells
    let head_line = method ++ " " ++ head
    let lines = head_line : y_cells
    withFile path WriteMode $ \handle -> do
        sequence_ $ map (hPutStrLn handle) lines

mk_html' :: Integer -> String -> (Integer, Integer) -> (Integer, Integer) -> IO ()
mk_html' n method (x_min, x_max) (y_min, y_max) = do
    x_cells <- gen_cells x_min x_max
    y_cells <- gen_cells y_min y_max
    print_table (i2path n) method x_cells y_cells

mk_html :: Integer -> IO ()
mk_html n
    | n `rem` 4 == 1 = mk_html' n "+" (1, 99) (1, 99)
    | n `rem` 4 == 2 = mk_html' n "−" (50, 99) (0, 49)
    | n `rem` 4 == 3 = mk_html' n "×" (1, 99) (0, 9)
    | otherwise      = mk_html' n "÷" (1, 99) (1, 9)

main = sequence_ $ map mk_html [1..20]

早速、リファクタして頂いたのでコードを公開

{-# LANGUAGE TemplateHaskell, QuasiQuotes #-}
import System.Random
import System.FilePath
import System.IO
import Data.List
import Control.Applicative
import Text.Hamlet
import Text.Blaze.Html.Renderer.String
import Text.Blaze

data Method = Plus | Minus | Mult | Div
type Range = (Integer, Integer)
type Cells = [Integer]
type Answers a = [[a]]

get_calc_fun :: Method -> Integer -> Integer -> Integer
get_calc_fun Plus  = (+)
get_calc_fun Minus = (-)
get_calc_fun Mult  = (*)
get_calc_fun Div   = div

method2str :: Method -> String
method2str Plus  = "+"
method2str Minus = "−"
method2str Mult  = "×"
method2str Div   = "÷"

gen_cells :: Range -> IO Cells
gen_cells range = (take 10 . randomRs range) <$> newStdGen

calc_answer :: Method -> Cells -> Cells -> Answers Integer
calc_answer method x_cells y_cells = let f = get_calc_fun method in
    [[f x y | x <- x_cells] | y <- y_cells]

null_answer :: Answers Integer -> Answers String
null_answer answers = map (map (const "")) answers

i2path :: String -> Integer -> FilePath
i2path p i = "." </> (p ++ show i) <.> "html"

-- Integer や String は ToMarkup のインスタンス
print_table :: ToMarkup a => FilePath -> Method -> Cells -> Cells -> Answers a -> IO ()
print_table path method x_cells y_cells answers = do
    let rows = zip y_cells answers
    writeFile path $ renderHtml [shamlet|
!!!
<head>
  <title>100cells
<body>
  <table>
    <tr>
      <th>#{method2str method}
      $forall x <- x_cells
        <th>#{x}
    $forall (y, zs) <- rows
      <tr>
        <th>#{y}
        $forall z <- zs
          <td>#{z}
    |]

mk_html' :: Integer -> Method -> Range -> Range-> IO ()
mk_html' n method x_range y_range = do
    x_cells <- gen_cells x_range
    y_cells <- gen_cells y_range
    let answers = calc_answer method x_cells y_cells
    print_table (i2path "a" n) method x_cells y_cells answers
    print_table (i2path "p" n) method x_cells y_cells $ null_answer answers

mk_html :: Integer -> IO ()
mk_html n
    | n `rem` 4 == 1 = mk_html' n Plus  ( 1, 99) (1, 99)
    | n `rem` 4 == 2 = mk_html' n Minus (50, 99) (0, 49)
    | n `rem` 4 == 3 = mk_html' n Mult  ( 1, 99) (0,  9)
    | otherwise      = mk_html' n Div   ( 1, 99) (1,  9)

main = mapM_ mk_html [1..20]