お風呂ライオン工場

まとめブログ

SQS標準キューでLambdaトリガーを再有効化したときの処理順について実験したよ!

SQSで送信順に処理をしたいならFIFOキューを使うと思います。
ですが、標準キューでもLambdaトリガーなどで処理をしている場合はだいたい送信順にメッセージを受信して処理してくれます。
SQSのデベロッパーガイドにも、標準キューの「メッセージ順序」のところに次のように記載があります。

標準キューでは、できる限りメッセージの順序を保持しますが、複数のメッセージのコピーが順無同で配信される場合があります。お使いのシステムで注文を保存する必要がある場合は、FIFO (先入れ先出し) キューするか、各メッセージに順序付け情報を追加して、受信時にメッセージを並べ直せるようにすることもできます。

今回はなんらかの理由によりLambdaトリガーを一度無効化して、その後再度有効化した場合に、標準キューの滞留したメッセージはどれくらい順序を保って処理されるのかを実験してみました。

目次

環境

環境はSQS標準キューとLambda関数を作成し、Lambdaのトリガーにキューを追加します。
関数のコードはsqs-pollerブループリントのJavaScriptコードを使わせてもらいます。
結果を確認しやすいようにLambdaの同時実行数は1にします。

f:id:kkmtyyz:20211016212532p:plain
環境図

CloudFormationのコード:

AWSTemplateFormatVersion: "2010-09-09"
Resources:
  MyQueue:
    Type: AWS::SQS::Queue
    Properties:
      QueueName: 'MyQueue'

  SqsPollerLambdaRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: 'SqsPoller'
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - 'lambda.amazonaws.com'
            Action:
                - 'sts:AssumeRole'
      Policies:
        - PolicyName: 'SqsPollerSQSPolicy'
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  - 'sqs:DeleteMessage'
                  - 'sqs:GetQueueAttributes'
                  - 'sqs:ReceiveMessage'
                Resource:
                  - !GetAtt MyQueue.Arn
        - PolicyName: 'SqsPollerLogPolicy'
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  - 'logs:CreateLogStream'
                  - 'logs:PutLogEvents'
                  - 'logs:CreateLogGroup'
                Resource:
                  - '*'

  SqsPollerLambda:
    Type: AWS::Lambda::Function
    Properties: 
      FunctionName: 'SqsPoller'
      Role: !GetAtt SqsPollerLambdaRole.Arn
      Handler: 'index.handler'
      Runtime: 'nodejs12.x'
      ReservedConcurrentExecutions: 1
      Code: 
        ZipFile:  |
          console.log('Loading function');
          
          exports.handler = async (event) => {
              //console.log('Received event:', JSON.stringify(event, null, 2));
              for (const { messageId, body } of event.Records) {
                  console.log('SQS message %s: %j', messageId, body);
              }
              return `Successfully processed ${event.Records.length} messages.`;
          };

  SqsPollerLambdaEventSourceMapping:
    Type: AWS::Lambda::EventSourceMapping
    Properties:
      Enabled: true  
      EventSourceArn: !GetAtt MyQueue.Arn
      FunctionName: !Ref SqsPollerLambda

  SqsPollerLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub '/aws/lambda/${SqsPollerLambda}'

実験

実験手順は次の通りです。

  1. メッセージ1から10を送信
  2. Lambda関数のログを確認(トリガー無効化前)
  3. トリガーを無効化
  4. メッセージ1から10を送信
  5. キューの滞留メッセージを確認
  6. トリガーを再度有効化
  7. Lambda関数のログを確認(トリガー最有効化後)

1. メッセージ1から10を送信

作成したキューにメッセージを送信します。

$ aws_account=`aws sts get-caller-identity | jq -r .Account`
$ for i in `seq 1 10`; do aws sqs send-message --queue-url "https://sqs.ap-northeast-1.amazonaws.com/${aws_account}/MyQueue" --message-body "$i"; done

2. Lambda関数のログを確認(トリガー無効化前)

LambdaトリガーによってCloudWatch Logsに書き込まれたログを確認します。
1から10のメッセージを送信された順に処理していることがわかります。

f:id:kkmtyyz:20211017155517p:plain
トリガー無効化前のログ

3. トリガーを無効化

Lambdaトリガーを無効化します。

$ uuid=`aws lambda list-event-source-mappings | jq -r '.EventSourceMappings[] | select(.FunctionArn | contains("SqsPoller")) | .UUID'`
$ aws lambda update-event-source-mapping --uuid $uuid --no-enabled
$ aws lambda get-event-source-mapping --uuid $uuid | jq .State
"Disabled" 

4. メッセージ1から10を送信

再びキューにメッセージを送信します。

$ for i in `seq 1 10`; do aws sqs send-message --queue-url "https://sqs.ap-northeast-1.amazonaws.com/${aws_account}/MyQueue" --message-body "$i"; done

5. キューの滞留メッセージを確認

キューにメッセージが滞留していて、Lambdaトリガーによって処理されていないことを確認します。

$ for i in `seq 10`; do aws sqs receive-message --queue-url "https://sqs.ap-northeast-1.amazonaws.com/${aws_account}/MyQueue" --max-number-of-messages 10 | jq -r .Messages[].Body; done
10
2
5
8
9
3
7
4
6
1

6. トリガーを再度有効化

キューの可視性タイムアウトが30秒なので、少し時間をおいてからLambdaトリガーを再度有効化します。

$ aws lambda update-event-source-mapping --uuid $uuid --enabled
$ aws lambda get-event-source-mapping --uuid $uuid | jq .State
"Enabled" 

7. Lambda関数のログを確認(トリガー最有効化後)

LambdaトリガーによってCloudWatch Logsに書き込まれたログを確認します。
手順5でreceive-messageを叩いたときに薄々そんな気はしてましたが、バラバラに受信されていることがわかります。

f:id:kkmtyyz:20211017160028p:plain
トリガー再度有効化後のログ

結果

結果としてはSQS標準キューにてメッセージを滞留させた状態でLambdaトリガーを再有効化すると、受信時には送信時の順序がバラバラになりました。
トリガーが有効化された状態では送信順に処理されたので、すごい頻度でポーリングしてるのかなと思いました。ポーリングのリクエスト毎に課金されるようなので、イベントを有効化した状態で放置してしまうとどんどん課金されてしまいそうです。

まとめ

そもそも標準キューで順番を気にするようなことをするなよと言えばそれはそうですが、いざ「だいたい送信順に処理されるって書いてあるけど、トリガーを無効化した後に再度有効化したらどうなるの?」って聞かれたときに困ってしまったので、実験してみた次第でした。
しかしCloudFormationを書くのはまだまだ時間がかかってしまいます。コンソールでは意識していないリソースもCloudFormationでは認識する必要があったり、設定もコンソールではサクサクできてもCloudFormationでは別のリソースとして定義しなければならなかったりと、まだまだ知らないことが多いなあと思います。
しかしそれが楽しいんですけどね😊

AWSの認定ぜんぶ取ったよ!(11冠)

じゃーん!!AWSのバッジです!!やったね😊

f:id:kkmtyyz:20211004213847p:plain
AWSバッジ

目次

動機

以前書いた入門しました記事から3か月ほど経って、まとまったお休みを得ることができたので、再びAWSの認定を取ることにしました。 kkmtyyz.hatenablog.com

Solutions Architect Professionalから先を取得しようと思ったのには次のような理由があります。

  1. ガラスの盾が欲しかった
  2. いつでも転職できる自信が欲しかった
  3. 単純にAWSに詳しくなりたかった
  4. 意識を逸らしたかった

理由1については、どうやら全ての認定をとると「APN ALL AWS Certifications Engineers」というアワードに応募出来て、運が良ければガラスの盾が貰えるらしく、ネットで見てカッコいいなーと思ったからですね。
理由2は、AWSの認定全部もってたらさすがに転職いつでもできる自信が付くだろうと思ったからです。もう今度辛くなったらいつでもドロップアウトできるように、食うのには困らない程度の自信になるといいなと思いました。
理由3は、以前の記事でCLFとSAAを取得してからAWSが結構好きになっていて、もっと詳しくなって遊べるようになりたいなと思ったからです。AWSは便利だし、開発の幅が広がるので、想像するだけでも楽しいですよね。
理由4は、いろんな辛い状態にあって、よくない思考がずっとぐるぐるして止まらなかったので、認定試験の勉強をすることで意識をそちらに逸らしたかった。結果として徐々に集中できるようになったので、この方法は良かったかなと思います。

取得スケジュール

取得スケジュールは画像の通りですが、7, 8, 9月はほぼ毎週受験していて、結構ストイックにできました。
勉強期間はほとんど1週間ですが、SAPとANSは長く勉強しました。

f:id:kkmtyyz:20211004223159p:plain
受験スケジュール

個人的な難易度ランキング

難易度は個人のバックグラウンドや取得順によって変わると思いますが、自分ではこんなランキングになりました。
ProfessionalとSpecialityは全部難しかったです。

f:id:kkmtyyz:20211004230047p:plain
個人的な難易度ランキング

勉強方法

やり方としては、問題文を読んで、ドキュメントやブログを読んで、動画を見て、実際に手を動かしてみる感じです。認定を取るのもそうだけど、ちゃんと使えるようにもなりたかったのでその辺の意識を忘れないようにしました。

必要な情報は全てweb上で見つけられるので、問題集があれば勉強することができます。
私は海外の3つくらいのサイトを使いました。1つの試験につき1つだけ問題集を買っていて、だいたい1500円くらいなので11試験で15000円くらいかかりました。
問題集の使い方は次のフローチャートの通りです。

f:id:kkmtyyz:20211005221149p:plain
問題の解き方

問題文や選択肢を読んでいて知らないサービスや用語が出てきた場合は都度調べます。
知らないサービスが出てきた場合は次の観点で調べます。調べている時間がもっとも長いので、購入したテスト問題は一部解き終わらないまま試験に臨むことが多かったです。

  1. 何ができるのか
  2. どうやって使うのか
  3. どんなサービスと連携できるのか
  4. 最近どんなアップデートがあったのか

1番は、「AWS サービス別資料」からBlackBeltの動画を見て概要を掴み、ユーザーガイドやデベロッパーガイドを読んで、細かな機能や設定項目を押さえます。特にProfessionalからは各ガイドを通して読むようになりました。ところどころにある「注意」等の記述は特に記憶に残るように読みました。
2番は、実際の設定方法を押さえます。コンソールでの設定方法やCloudFormationの書き方を身に着けます。問題文と同様の環境を作ってみたりするのが楽しいのでオススメですが、時間が無いときや実際に触れないサービスとかは設定画面のスクショを載せてる「使ってみた」系のブログ記事を探して読んだりしました。スクショの本題ではない設定項目とかにも目を光らせていると、知らない設定を見つけたりするのでドキュメントに確認しに行く良い機会になります。
3番と4番はサービス名でググって個人や企業がやってるブログ記事を読むことが多かったです。DevelopersIOはサービス名で検索できるしいろんな種類の記事が沢山あるので非常に便利でした。

あとは犬の散歩中やお風呂のときとかに「あれって暗号化は?バックアップは?あのサービスと直に連携する方法あるかな?」とか色々考えるようになったりするので、それを後から調べたり、ご飯たべてるときにyoutubeAWS Japanチャンネルの動画を見たりしてました。

SOAは私が受験するときにちょうどC02になって、ラボ試験が追加されました。ネットに何も情報が無いまま受験しましたが、実際に手を動かして遊んでいたおかげか特に躓くことはありませんでした。すごく緊張しましたが。

受かると貰える特典

試験に受かるといろいろな特典が貰えます。
その中でも半額クーポンは特に良くて、次に受ける試験が半額で受けられます。私はお金が無いので、受かった次の試験は必ず半額クーポンを使って受けてました。 Professional以上は1回33000円くらいかかるので、半額でも結構な出費ですが......。
他にも模擬試験1回無料クーポンなんかも貰えますが、私は結局一度も使いませんでした。

まとめ

短いようで長かったなと思います。意識を逸らすという目的には本当にぴったりでした。合格した時は嬉しくなって、不合格のときはすごく悔しくて、試験のときは毎回すごく緊張して、楽しかったです。AWSに関する自信は認定を取得すればするほど無くなっていき、気持ちとしてはやっとこれで入門が始められるという感じがします。
これから仕事でも多少なりともAWSを触る機会が増えそうなので、今回得た知識を生かしていければなと思います。
また、他にも勉強したいことが山ほどあるし、本も溜まってきてるのでそろそろAWSじゃないこともまた初めていこうと思います。

parsercherがv3.1.0になったよ!

parsercher v3.1.0

前回のリリース記事「Rustでタグをパースするクレートを公開したよ! - ハッピー抹茶工場」からバージョンアップしてv3.1.0をリリースしました。
いくつかAPIを追加したりしたので今回はその辺を書こうと思います。変更点はgithubのReleasesやCHANGELOG.mdに記載しています。

github.com

目次

部分木の検索ができるようになりました

Dom構造体ツリーから別のDom構造体ツリーを使用して部分木を検索できます。
これにはparsercher::search_dom()を使用します。
次の例はdocからneedleと一致する部分木を取得するものです。 list1list3が一致します。
needle:

  <ul class="targetList">
    <li class="key1"></li>
    <li class="key2"></li>
  </ul>

例:

let doc = r#"
  <body>
    <ul id="list1" class="targetList">
      <li class="key1">1-1</li>
      <li class="key2"><span>1-2</span></li>
    </ul>

    <ul id="list2">
      <li class="key1">2-1</li>
      <li>2-2</li>
    </ul>

    <div>
      <div>
        <ul class="targetList">
          <ul id="list3" class="targetList">
            <li class="key1">3-1</li>
            <li class="item">3-2</li>
            <li class="key2">3-3</li>
          </ul>
        </ul>
      </div>
    </div>

    <ul id="list4">
      <li class="key1">4-1</li>
      <li class="key2">4-2</li>
    </ul>
  </body>
"#;

let doc_dom = parsercher::parse(&doc).unwrap();

let needle = r#"
  <ul class="targetList">
    <li class="key1"></li>
    <li class="key2"></li>
  </ul>
"#;
let needle_dom = parsercher::parse(&needle).unwrap();
// Remove `root`dom of needle_dom
let needle_dom = needle_dom.get_children().unwrap().get(0).unwrap();

if let Some(dom) = parsercher::search_dom(&doc_dom, &needle_dom) {
    parsercher::print_dom_tree(&dom);
}

出力:

<root>
  <ul id="list1" class="targetList">
    <li class="key1">
      TEXT: "1-1"
    <li class="key2">
      <span>
        TEXT: "1-2"
  <ul class="targetList" id="list3">
    <li class="key1">
      TEXT: "3-1"
    <li class="item">
      TEXT: "3-2"
    <li class="key2">
      TEXT: "3-3"

タグの属性を検索できるようになりました

全てのタグから任意の属性の値を取得できます。
これにはparsercher::search_attr()を使用します。
複数の属性を指定できるparsercher::search_attrs()もあります。
例えば、全てのid属性から値を取得する場合は次のようにします。

let doc = r#"
  <head>
    <meta charset="UTF-8">
    <meta id="value1">
    <title>sample html</title>
  </head>
  <body id="value2">
    <h1>sample</h1>

    <div id="value3"></div>

    <ol>
      <li>first</li>
      <li id="value4">second</li>
      <li>therd</li>
    </ol>
  </body>
"#;

let dom = parsercher::parse(&doc).unwrap();

let values = parsercher::search_attr(&dom, "id").unwrap();
assert_eq!(values.len(), 4);
assert_eq!(values[0], "value1".to_string());
assert_eq!(values[1], "value2".to_string());
assert_eq!(values[2], "value3".to_string());
assert_eq!(values[3], "value4".to_string());

id属性に加えてclass属性の値も取得する場合は次のようにします。

let doc = r#"
  <head>
    <meta charset="UTF-8">
    <meta id="id1">
    <title>sample html</title>
  </head>
  <body id="id2" class="class1">
    <h1>sample</h1>

    <div align="center" class="class2"></div>

    <ol>
      <li>first</li>
      <li class="class3">second</li>
      <li>therd</li>
    </ol>
  </body>
"#;

let dom = parsercher::parse(&doc).unwrap();

let attrs = vec!["id", "class"];
let values = parsercher::search_attrs(&dom, &attrs).unwrap();
assert_eq!(values.len(), 5);
assert_eq!(values[0], "id1".to_string());
assert_eq!(values[1], "id2".to_string());
assert_eq!(values[2], "class1".to_string());
assert_eq!(values[3], "class2".to_string());
assert_eq!(values[4], "class3".to_string());

木の十分条件を評価できるようになりました

木の十分条件を評価するDom::p_implies_q_tree()を追加しました。 部分木の検索はこれにより実現されています。
また、Dom構造体の十分条件を評価するDom::p_implies_q()を追加したほか、以前までタグの十分条件を評価するためにあったparsercher::satisfy_sufficient_condition()Tag::p_implies_q()に変更しました。

以下は木の十分条件を評価する例です。

let p = r#"
  <body>
    <div></div>
    <ul>
      <li class="item"><li>
    </ul>
  </body>
"#;
let p_dom = parsercher::parse(&p).unwrap();

let q = r#"
  <body>
    <div id="content"></div>
    <ul id="liset1">
      <li class="item">item1<li>
      <li class="item">item2<li>
      <li class="item">item3<li>
    </ul>
  </body>
"#;
let q_dom = parsercher::parse(&q).unwrap();

assert_eq!(Dom::p_implies_q_tree(&p_dom, &q_dom), true);

domモジュール下の構造体が使いやすくなりました

  • Tag, Text, Comment構造体について、コンストラクタの引数の型がStringから&strになりました。
  • Tag構造体の属性設定がより簡単に記述できるようになりました。従来のHashMapを使用する方法はTag::set_attrs()として残っています。
// <div id="section" class="alert alert-primary">
let mut tag = Tag::new("div");
tag.set_attr("id", "section");
tag.set_attr("class", "alert alert-primary");
  • PartialEqを継承しました。Dom構造体も含むため、木を等価演算子で比較できます。
let a = r#"
<head>
  <title>sample</title>
</head>
<body>
  <h1>section</h1>
  <ul>
    <li>list1</li>
    <li>list2</li>
  </ul>
</body>
"#;
let a_dom = parsercher::parse(&a);

let b = r#"
<head>
  <title>sample</title>
</head>
<body>
  <h1>section</h1>
  <ul>
    <li>list1</li>
    <li>list2</li>
  </ul>
</body>
"#;
let b_dom = parsercher::parse(&b);

assert_eq!(a_dom == b_dom, true);
assert_eq!(a_dom != b_dom, false);

まとめ

API追加の合間に既存API名の変更を行っていたので短期間にメジャーバージョンが3まで来ました。思い付きで楽しく作っていて都度リリースしているので、セマンティックバージョニングを採用しているとメジャーバージョンが上がりがちです。crates.ioの他のクレートを見ていると同じことになっているものもあるので、そんなもんかなと思います。数字が大きくなるのはスマートではないけれど別に悪いことではないし、バージョン番号は差異があることを示すための数字に過ぎない気もします。

自分は会社や仕事のことがずっと頭から離れないのですが、最近は「あとはどんな機能があったら便利かな」と考えたりすることもあって、いい気分転換になっている気がします。

Rustでタグをパースするクレートを公開したよ!

parsercher

parsercherというクレートを公開しました。
HTMLやXMLといったタグで記述されたドキュメントをパースし、タグやテキストを抽出することができます。 パース結果は構造体のツリーとして返してくれるので、APIによる操作の他に自分でツリーを操作することができます。

(追記: v3.1.0の記事を書きました

github.com

目次

使い方

APIの使い方や出力例は上のdocs.rsに記載しています。 また、ソースコードexamplesAPI毎の例を用意しています。

ここでは簡単な使用例と共にparsercherを紹介します。

まずはCargo.tomlにparcherserを加えましょう。

[dependencies]
parsercher = "1.0.0"

例に使用するHTML

今回は次のHTMLを例として使用します。ファイル名はindex.htmlです。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>sample</title>
  </head>
  <body>
    <h1>Hello, world!</h1>

    <div id="content"></div>

    <ul id="list1">
      <li class="listItem">リンゴ</li>
      <li class="listItem">ゴリラ</li>
      <li class="listItem">ラッパ</li>
    </ul>

    <ol id="list2">
      <li id="target" class="listItem">first</li>
      <li class="listItem">second</li>
      <li id="target" class="listItem">therd</li>
    </ol>

    <!-- All script code becomes one text -->
<script>
  let content = document.getElementById('content');
  content.textContent = 'content';
</script>

  </body>
</html>

ドキュメントをパースする

index.htmlをパースして構造体のツリーを作りましょう。 これにはparsercher::parse()を使用します。
以下はパースした結果を表示する例です。

use std::fs::OpenOptions;
use std::io::prelude::*;
use parsercher;

fn load_input() -> String {
    let mut f = OpenOptions::new().read(true).open("index.html").unwrap();
    let mut input = String::new();
    f.read_to_string(&mut input).unwrap();
    input
}

fn main() {
    let input = load_input();
    if let Ok(dom) = parsercher::parse(&input) {
        println!("{:#?}", dom);
    }
}

標準出力にはDom構造体のツリーが表示されます。長すぎるのでheadタグからmetaタグまでを抜き出すと、以下のようになっています。(似たような出力の全体像がドキュメントの例にあります)

Dom {
    dom_type: Tag,
    tag: Some(
        Tag {
            name: "head",
            attr: None,
            terminated: false,
            terminator: false,
        },
    ),
    text: None,
    comment: None,
    children: Some(
        [
            Dom {
                dom_type: Tag,
                tag: Some(
                    Tag {
                        name: "meta",
                        attr: Some(
                            {
                                "charset": "UTF-8",
                            },
                        ),
                        terminated: false,
                        terminator: false,
                    },
                ),
                text: None,
                comment: None,
                children: None,
            },

ツリーをprintln!マクロで表示したのでは正しくパースが行えているか確認することが困難なので、そんなときはparsercher::print_dom_tree()が便利です。main関数を次のように変更します。

fn main() {
    let input = load_input();

    if let Ok(dom) = parsercher::parse(&input) {
        parsercher::print_dom_tree(&dom);
    }
}

今度はDom構造体のツリーが見やすく出力されます。

<root>
  <!DOCTYPE html="">
  <html>
    <head>
      <meta charset="UTF-8">
      <title>
        TEXT: "sample"
    <body>
      <h1>
        TEXT: "Hello, world!"
      <div id="content">
      <ul id="list1">
        <li class="listItem">
          TEXT: "リンゴ"
        <li class="listItem">
          TEXT: "ゴリラ"
        <li class="listItem">
          TEXT: "ラッパ"
      <ol id="list2">
        <li id="target" class="listItem">
          TEXT: "first"
        <li class="listItem">
          TEXT: "second"
        <li id="target" class="listItem">
          TEXT: "therd"
      <!--" All script code becomes one text "-->
      <script>
        TEXT: "\n  let content = document.getElementById('content');\n  content.textContent = 'content';\n"

Dom構造体は次のような定義になっており、タグはTag構造体、コメントはComment構造体、それ以外のテキスト部分はText構造体になります。ドキュメント内で<script></script>で囲まれたコード部分はscriptタグの子のテキストになります。

pub struct Dom {
    /// Type of Dom structure
    pub dom_type: DomType,
    tag: Option<Tag>,
    text: Option<Text>,
    comment: Option<Comment>,
    children: Option<Vec<Box<Dom>>>,
}

タグ名でタグを抽出する

指定したタグ名に一致するタグを抽出することができます。
これにはparsercher::search_tag_from_name()を使います。
例えば、liタグを全て抽出するなら次のようにします。

fn main() {
    let input = load_input();

    if let Ok(dom) = parsercher::parse(&input) {
        if let Some(tags) = parsercher::search_tag_from_name(&dom, "li") {
            for (i, tag) in tags.iter().enumerate() {
                println!("{}: {:?}", i, tag);
            }
        }
    }
}

結果はTag構造体のVecで返されます。

0: Tag { name: "li", attr: Some({"class": "listItem"}), terminated: false, terminator: false }
1: Tag { name: "li", attr: Some({"class": "listItem"}), terminated: false, terminator: false }
2: Tag { name: "li", attr: Some({"class": "listItem"}), terminated: false, terminator: false }
3: Tag { name: "li", attr: Some({"id": "target", "class": "listItem"}), terminated: false, terminator: false }
4: Tag { name: "li", attr: Some({"class": "listItem"}), terminated: false, terminator: false }
5: Tag { name: "li", attr: Some({"id": "target", "class": "listItem"}), terminated: false, terminator: false }

特定のタグを抽出する

タグ名や属性、値といった条件を満たすタグを抽出することができます。
これにはparsercher::search_tag()を使います。
例えば、id属性の値がtargetであるliタグを全て抽出するなら次のようにします。

use parsercher::dom::Tag;

fn main() {
    let input = load_input();

    if let Ok(dom) = parsercher::parse(&input) {
        // <li id="target">
        let mut tag = Tag::new(String::from("li"));
        let mut attr = HashMap::new();
        attr.insert(String::from("id"), String::from("target"));
        tag.set_attr(attr);

        if let Some(tags) = parsercher::search_tag(&dom, &tag) {
            for (i, tag) in tags.iter().enumerate() {
                println!("{}: {:?}", i, tag);
            }
        }
    }
}

結果はTag構造体のVecで返されます。

0: Tag { name: "li", attr: Some({"class": "listItem", "id": "target"}), terminated: false, terminator: false }
1: Tag { name: "li", attr: Some({"class": "listItem", "id": "target"}), terminated: false, terminator: false }

特定のタグの子テキストを抽出する

タグ名や属性、値といった条件を満たすタグを指定し、その子となるテキストのみ抽出することができます。
これにはparsercher::search_text_from_tag_children()を使います。
例えば、id属性の値がtargetであるliタグの子テキストを全て抽出するなら次のようにします。

use parsercher::dom::Tag;

fn main() {
    let input = load_input();

    if let Ok(dom) = parsercher::parse(&input) {
        // <li id="target">
        let mut tag = Tag::new(String::from("li"));
        let mut attr = HashMap::new();
        attr.insert(String::from("id"), String::from("target"));
        tag.set_attr(attr);

        if let Some(texts) = parsercher::search_text_from_tag_children(&dom, &tag) {
            for (i, text) in texts.iter().enumerate() {
                println!("{}: {:?}", i, text);
            }
        }
    }
}

結果はStringのVecで返されます。

0: "first"
1: "therd"

タグ同士の十分条件を判定する

pタグがqタグであるための十分条件であることを判定することができます。 Dom構造体のツリーを自分で操作する際に便利です。
これにはparsercher::satisfy_sufficient_condition()を使います。 結果はboolで返します。
例えばp=>qの場合、次のようになります。

use parsercher::dom::Tag;

fn main() {
    // <h1 class="target">
    let mut p = Tag::new("h1".to_string());
    let mut attr = HashMap::new();
    attr.insert("class".to_string(), "target".to_string());
    p.set_attr(attr);

    // <h1 id="q" class="target">
    let mut q = Tag::new("h1".to_string());
    let mut attr = HashMap::new();
    attr.insert("id".to_string(), "q".to_string());
    attr.insert("class".to_string(), "target".to_string());
    q.set_attr(attr);

    assert_eq!(parsercher::satisfy_sufficient_condition(&p, &q), true);

    // <h1 id="q">
    let mut q = Tag::new("h1".to_string());
    let mut attr = HashMap::new();
    attr.insert("id".to_string(), "q".to_string());
    q.set_attr(attr);

    assert_eq!(parsercher::satisfy_sufficient_condition(&p, &q), false);
}

まとめ

パーサは書いていて楽しいですね。オライリーの言語実装パターンをまた読み直したいと思いました。

parsercherという名前はparserとsearcherから作った造語です。いままでHTMLを操作するときはgrepsedを使ってある程度削ってから都度プログラムを書いていましたが、今後はparsercherを使っていきたいなと思います。検索の機能がまだ弱いので、Dom構造体ツリーの部分一致するところだけ柔軟に抽出する機能とかを作りたいですね。

今回初めてcrates.ioにクレートを公開しましたが、公開と共にdocs.rsにドキュメントページが作成されるのは大変便利だと思いました。Doc commentsを書こうという意識が生まれます。

DDLからコード生成するツールを作ったよ!

MySQLDDLをパースして外部キー制約を考慮したDELETEとINSERTのコード片を生成するツールを作りました。

github.com

Github Pagesからも使えます↓
https://kkmtyyz.github.io/ddl-tool/

経緯

外部キー制約が定義されたデータベースを使う機会があり、テストコードを書く際に毎回レコードの削除と挿入を行うコードを書く必要がありました。
あるテストで使用するテーブルは3つだけでも、外部キー制約から20個のテーブルに関して削除と挿入のコードを書く必要があり、当然それら操作の順序を考慮する必要があります。使用するテーブルの組み合わせはAPI毎に異なるので都度テーブル定義から依存関係を確認する必要があり、手間とミスが生じるのでツールを作りました。

動作

例えば、MySQLのサンプルデータベースsakilaDDLを開いてaddressテーブルとfilmテーブルを選択すると、次のようなコード片が作れます。

出力はgenDeleteTableCode関数とgenInsertJavaCode関数を編集することで変更できます。

f:id:kkmtyyz:20210425214945p:plain

// delete tables
Address
Film
City
Language
Country

// insert entities
CountryEntity insertCountryEntity = new CountryEntity();
// insertCountryEntity

LanguageEntity insertLanguageEntity = new LanguageEntity();
// insertLanguageEntity

CityEntity insertCityEntity = new CityEntity();
insertCityEntity.setCountryId(insertCountryEntity.getCountryId());
// insertCityEntity

FilmEntity insertFilmEntity = new FilmEntity();
insertFilmEntity.setLanguageId(insertLanguageEntity.getLanguageId());
insertFilmEntity.setOriginalLanguageId(insertLanguageEntity.getLanguageId());
// insertFilmEntity

AddressEntity insertAddressEntity = new AddressEntity();
insertAddressEntity.setCityId(insertCityEntity.getCityId());
// insertAddressEntity

まとめ

チームの人々が喜んでくれたので良かったです。
普段JavaScriptはあまり触りませんが、ちょっとしたツールが欲しいときに簡単にGUIを付けられるので便利ですね。
こういうツールは必要になったその時限りの出番になることが多いので、今後もこのツールを拡張して使えるような機会があるといいなと思います。

親知らず抜いたよ! (4本/日)

親知らずを4本、1日で抜歯した際の記録。

抜歯前の私が知り得たら有益だったであろうことも書いたため、これから親知らずを抜歯しようと思っている人にも参考になる部分があるかもしれない。

 

目次

1. 経緯

私には上下左右それぞれ1本ずつ合計4本の親知らずが生えており、向きは幸い全て真っ直ぐだった。上2本は完全に歯茎から露出しているが、下2本は歯茎に半分埋没している。日常生活を送る上で特に支障はなく、歯科検診でも抜歯の話は出たことがなかった。

1月上旬、徐々に左の親知らずが痛み始め、最初はズキズキとした僅かな痛みだったが、3, 4日経過して口が開けられないほどの痛みに発展した。歯が痛いというよりは歯茎、顎が痛いというような具合で、歯茎が赤く腫れていることが目視で確認できた。食事はなんとかできたが、辛い痛みで夜は寝付けず、何にも集中することができなかった。

原因は親知らずの歯茎から細菌が入ったことによる炎症で、通算2度歯医者に通い2週間程度かけて治療を行った。医師から「親知らずが存在する限り同様の事態が今後も起こりうる」と言われ、人生で初めて親知らずの抜歯を決意した。

ここでは抜けないので大きな病院に紹介状を書くと言われ、近所の総合病院か大学病院の選択肢を出された。日程の都合をこちら側でつけられるという理由から総合病院を選択した。


2. 診察

2月上旬、紹介状を持参して総合病院で診察を受けた。

幸い口腔外科の担当医は気さくで会話がしやすく好印象だった。紹介状に何がどこまで記述されているのかは不明でそれをどこまで読んでいるのかも分からないが、まずは抜歯する動機を尋ねられた。炎症を起こして苦しんだ経緯を説明すると、医師は至極真っ当な理由だねと言って抜歯に賛成であることを述べた。次に医師は私の親知らずを含む口腔内の状態を確認し、抜歯に当たる注意事項や複数の施術プランについて流暢に説明してくれた。複数の資料を用いた詳細な説明にもかかわらず非常に分かりやすく、同様の説明を相当な回数行ってきたことが窺えた。親知らずの抜歯にはいくつかリスクがあり、当然それらについても丁寧に説明される。神経に関する痺れ等は一生残る可能性もあるらしく、不安症な私は抜歯当日まで怯えて過ごすことになった。

親知らず抜歯におけるリスクの例

  • 抜歯後の穴に溜まる血の塊がうがい等により取れてしまうと、ドライソケットという骨が露出した状態になり痛みが続く可能性がある。
  • 下の親知らずを抜歯する場合、下顎骨を通っている神経が損傷して下唇に痺れや麻痺等の知覚障害が出現する可能性がある。
  • 上の親知らずは解剖学的に上顎洞という骨の空洞に突き抜けている場合があるため、抜歯により口とその空洞が貫通してしまい二次的に閉鎖する手術が必要になる可能性がある。

医師は1日の入院で4本全ての親知らずを抜歯するプランを推薦した。私はここへ来るまでの事前情報として「シュタゲのオカリンが親知らずを抜くSS」を読んでいたため、4本同時はヤバいことを知っていた。左右で2回に分けて2本ずつ抜歯してほしいと言うと、医師はその理由を尋ね、私は片方ずつであれば抜歯していない側で咀嚼できることをインターネットで知ったからだと答えた。医師はそれを肯定した上で推薦は4本同時の抜歯であると再度言った。インターネットの医療情報は誇張や嘘が多いこと、目の前にいるのはその道のプロであり施術プランやリスクの過不足ない説明振りからも今まで数多の親知らずを抜いてきたであろうこと、それに心の弱い自分では1度目の抜歯がトラウマとなり2度目の抜歯に挑めない可能性があることなどいろいろ勘案し、入院して4本抜歯することを決めた。結果的にその選択は正しかったと思う。

入院して4本の親知らずを抜歯するプランのメリット

  • 抗生剤や痛み止め等の点滴を翌日退院時まで受けられる
  • 抜歯後に不測の事態が発生しても病院なのですぐに対応できる
  • 抜歯後すぐベッドで休める
  • 抜歯後の状態でも食べられる食事が用意される
  • 抜歯後の辛く苦しい1週間が1度で済む

また、これは病院によるとも思うが、抜歯当日に抜歯する本数を減らすことが可能。しかし逆に増やすことはできない。

診察の最後に、レントゲンから親知らずが顎の神経に触れている可能性がある旨を伝えられ、CTを撮ることによりその状態をより明確に捉えることが可能であるがどうするかと聞かれた。CTは安くないが、リスクに怯える自分を少しでも安心させるために撮ることにした。


3. 入院、そして抜歯

4月上旬、入院当日。診察から2カ月程度経っているが、その間には特に何もなかった。ただ恐怖だけがあった。2カ月は長いように思うが、診察日時点で選択可能な最短の日程が2か月後だった。

朝食をとり病院へ行ってPCR検査をして入院手続きを行った後、病室へ案内される。8人部屋で私を含めて6人が入院していた。病室へ着くとすぐに入院着に着替え、点滴のためのチューブと識別バンドが腕に付けられた。すぐにお昼になり、入院して最初の食事が提供された。この普通に美味しい病院食の味をその後1週間のうちに幾度も思い出すことになるとは思いもしなかった。

f:id:kkmtyyz:20210418215415j:plain
f:id:kkmtyyz:20210418214630j:plain


食事が終わると抗生剤の点滴が始まり、不安はピークに達した。持参した「最終人類 上」が全く頭に入ってこなかった。そしていよいよ看護師に連れられ口腔外科へ。

医師は他愛ない会話で私の緊張を和らげながら、以前撮ったCTを見せて親知らずと神経が触れてはいないが接近していることを教えてくれた。決断を迫るように4本抜歯することを私に最終確認し、私も意を決してお願いしますと言った。私がひどく緊張していることを察して明るく振舞ってくれるが、「大丈夫」などと一言も言わないところはプロだと思った。椅子が倒れ施術が始まった。

棒状の物が4本、それぞれ親知らずと頬の間に差し込まれる。それは表面から行う麻酔で、唾液に溶け出すため飲み込まないようにと説明される。棒が取り除かれ唾液が吸引されると、下の親知らず部分に鋭く刺さる感覚、同時に激しい痛み、ついにメスによる切開が始まったかと思った。痛みに耐えていると他の箇所にも同様の感覚、激しい痛み、思わず声が漏れる。すると椅子が起こされた。もう一本くらい抜きましたかと尋ねると、医師からはまだ麻酔だけだと返され、注射を刺しただけでこの痛みなら麻酔が効いているとはいえ切開なんてしたら自分はどうなってしまうのか想像できなかった。きっと通常よりも痛がっていたからだろう、医師が点滴による麻酔も追加してくれた。その時点から意識が徐々に遠のいていった記憶がある。再び椅子が倒され、左下の親知らずにものすごい力が加えられて激しい痛みと共に声にならない声で絶叫した。切開の感覚はなく、突然工具的なものでかけられる力のみを感じ、その力の方向も分からず音は自分の絶叫だけが聞こえていた。記憶が定かでないが、想像していたペンチ的なものではなかったように思う。一か所が終わるとすぐ次に取り掛かっていたように思う。次に力を感じたのは右下の親知らずで、これは歯に対してたがねをハンマーで打ち付けるような感覚だった。瞬間的な激しい痛みが複数回あり、絶叫した。縫合と上の2本を抜いた記憶は無い。経過した時間も分からないが、体感としては数分程度だったように思う。施術が完了し椅子が起こされたときには意識が非常に朦朧としており、車椅子に移されたことと廊下のリノリウムの反射光のみが記憶に残っている。

看護師の呼びかけで目が覚めた。いつの間にかベッドで寝ていて、酔っぱらって帰った次の朝みたいな感覚だった。親知らず付近は麻酔が効いている感覚だけで痛みはなかった。点滴を換えた看護師が抜歯した親知らずを渡してくれた。片側が透明なフィルムになっている紙袋には血の付いた歯が5つ入っており、その大きさに感嘆するとともになぜか誇らしい気持ちになった。最も大きいのは左下の親知らずで、親指の爪ほどもある。これが右下のそれでないことは、2つに割れた同じくらい大きな歯と施術時のたがねのような痛みから想像できた。あの打ち付ける痛みは歯を割っていたときのものだったのだろう。

感慨に浸っていると夕食の時間になった。ここから抜糸までの1週間、ドライソケットに怯えて親知らず跡地の血に常に気を配る生活が始まる。

抜糸当日の夕食(左)と翌朝の朝食(右)

f:id:kkmtyyz:20210418230223j:plain
f:id:kkmtyyz:20210418230350j:plain

 

病院食はよく出来ていて、咀嚼できない自分でもおいしく食べられるもので構成されていた。特に美味しかったのは朝食(写真右)で提供された魚の形をした食べ物で、非常に柔らかい魚肉の練り物。 ちゃんと魚の味がして感動した。

歯磨きは歯磨き粉を使用せず、うがいも軽く済ませるようにと言われていたので、磨けていない不快感は残ったが、それよりもドライソケットへの恐怖が勝った。

消灯は9時、施術後ずっと眠っていたせいかなかなか眠れなかった。同室の誰かのイヤホンから漏れているような聞き取れない野球中継だけがずっと響いていた。なぜか少し安心する雰囲気だった。夜中1時頃、麻酔が切れたのか痛みで目が覚めた。トイレのついでに痛み止めをもらい、再び眠った。

翌朝再び痛み止めの点滴を受け、医師の診察を経て退院となった。


4. 退院後の生活

金曜日に入院したので、土曜日の昼には帰宅した。抜糸は次の土曜日に予定されており、湯舟に浸かるのは4日後程度から、歯磨き粉の使用もその程度から、過度な運動は避けるよう指示を受けた。薬は痛み止めを含めて4種類が処方された。顔の腫れは3, 4日後にピークが来るだろうとのことだった。血圧の上昇から血餅が取れてしまうことを懸念し、湯舟は念のため抜糸まで我慢しようと決めた。

痛みは常にあり、固形物は食べることができないため、お粥やゼリーが主食となった。1日3食毎回お粥を食べると3回目にはもう飽きてくるので、炒めたケチャップをかけたり卵をかけたりして変化をつけた。特にお粥に合わせておいしかったのは海苔の佃煮と、かつおでんぶ。ゼリーは食物繊維が取れるタイプのものとカロリーが取れるタイプのものを重宝した。痛み止めはお腹が緩くなるため、食物繊維が取れると少し安心できる。苺やバナナを牛乳とミキサーにかけた飲み物もおいしかった。

f:id:kkmtyyz:20210418234735j:plain
f:id:kkmtyyz:20210418234855j:plain

最初の2日間は土日で休みということもあり、ずっとベッドで横になっていた。本を読んだりもしたが、痛みで集中できず、少しイライラしてしまうためyoutubeで雑談配信を聞きながらずっと寝ていた。夜になると痛みがひどくなるため痛み止めを飲んだ。抜歯当日ほどではないため、睡眠に支障を来すような痛みではない。摂取カロリーが少ないせいか気温がただ低いせいか分からないが、ずっと寒気があり就寝時も暖房をつけていた。

抜歯から3日目、月曜日なので普通に仕事が始まってしまう。痛みは徐々に引いているものの、依然として口は痛みであまり開かず、咀嚼する度に痛むため固いものは食べられない。やや顔が腫れてきた。幸い私は家で働いているので、お昼休みは薬を飲んだ後ベッドで休むことができた。ミーティングの時だけボソボソと喋っていたが、乗ってくるとあれやこれやと話すことが出てくるため結局沢山しゃべってしまった。顎の疲れと痛みが後から押し寄せ、後悔と苛立ちが生じた。早く治したいので仕事以外は寝る生活にした。

抜歯から5日目、夜精神安定のためにマリカーをしていたら舌の上にコロコロしたものが現れ、まさかと思いティッシュに出してみると案の定ブヨブヨした血の塊だった。これは取れてはいけないものなのではないだろうか、もうドライソケットで痛みの延長戦が確定だろうか、不安は加速した。その夜はチョコレートや唐揚げの夢を見た。人生で初めて食事をする夢を見て、今まで自分は食に淡泊だと思っていただけに驚いた。自分の新しい面を知るのはいつも唐突だ。

抜歯から6日目、朝の時点で抜歯による恒常的な痛みはなくなっていた。顔の腫れも落ち着いてきており、痛みとしては顎を動かした際の抜歯付近の痛みと、なぜかわからない下犬歯の歯茎付近の鈍痛のみとなった。

抜歯から7日目、物が食べられないことや、食事の際の痛みが治らないこと、お風呂に入れないこと、仕事の鬱憤などからイライラがピークに達し、怒りに任せて生姜焼きを口に頬張るも縫合箇所付近の激痛から泣く泣く吐きだし、悲しみのまま痛み止めと精神安定剤睡眠薬(別件で飲んでる)を服用して寝落ちした。


5. 抜糸

4月中旬、抜歯から1週間後の抜糸当日。

寝落ちしてしまい目覚ましをかけられなかったためやや寝坊したが、予約の5分前程度に口腔外科に到着。外は雨がひどく降っており不穏さが演出されていた。

医師はいつもの調子でここ1週間の様子を尋ねてきた。辛かった、何が辛かった、食べられないことが辛かった、咀嚼すると奥歯で痛みが走る。医師が口内を確認すると、どうやら縫合糸を奥歯が噛んでいる他、腫れていた頬も噛んでしまっていたことが原因らしく、抜糸をすると幾分良くなるのではなかろうかとのことだった。抜歯時ほどではない抜糸の施術確認が行われ、私がお願いしますと言うと椅子が倒れた。抜糸はすぐに終わった。痛かったが、これまでの親知らず抜歯の過程で最も痛くない痛みだった。情けない声も出していない。単純にその程度の痛みだっただけか、あるいは私がこの壮絶な抜歯施術を乗り越えてレベルアップした結果なのかもしれない。抜糸が終わるとアルコールで口を濯ぐよう言われ、奥歯まで濯いでもいいか確認すると許可されたため、うれしくなって奥歯まできっちり濯いだ。親知らず跡地の穴が水の流れを変化させ、新鮮な感覚を楽しく感じた。また違和感や恒常的な痛みも改善された。医師からドライソケットにもなっておらず何も問題がないことを告げられ、もう普通に生活してもよいと言われた。お風呂も、大丈夫、運動も、大丈夫、うがいも、大丈夫。暴れたって大丈夫だよと言われた。状態を確認するための次の検診は一か月後に決まった。

病院を出て、 まだ土砂降りだったけど、すごくいい気分だった。

ハンバーガーを買って帰った。

f:id:kkmtyyz:20210419010048j:plain

ポテトってこんなに美味しかったんだ。


6. まとめ

今回の経験に基づいた個人的な結論です。

  • もうこんな苦しみは味わいたくないので1回で4本抜歯してよかった
  • CTは安心できるから撮っておいてよかった
  • 点滴や病院食の点から入院してよかった
  • お粥には海苔の佃煮やかつおでんぶが合う
  • ゼリーは食物繊維が入ってるとよい
  • 抜歯から抜糸までは非常につらい
  • もしあなたが親知らずを抜歯するのであれば、心身ともに余裕のある時期に1週間程度の余暇をとって行うとよいかもしれません

AWSに入門しました

3月になり仕事がひと段落したので以前から気になっていたAWSに入門しました。

クラウドと呼ばれるものに触れるのは昨年春にOpenShiftのトレーニング(DO180DO280)を受けて以来です。DO280はOpenShiftについてある程度理解していることを前提に作成されているため、初見では納得のいく理解ができず、Kubernetesを勉強してからリトライしました。そのときはKubernetes完全ガイドを実践しましたが、環境はGCPではなく6つのRaspberry Pi 2を使ってHA構成のKubernetesを作って勉強したため、クラウドは使用しませんでした。各プログラムのバージョンやRAMの制限等から、HA構成を作るのはなかなか難しくて、ちゃんと動作したときはとても嬉しかったのを覚えています。

AWSは右も左も分からない状態だったため、とりあえず公式サイトを覗いてみたところ、それはもう膨大なサービスが存在していて驚嘆しました。若干引いてしまいましたが、まずは一般的に使用されているサービスを知る必要があるので、体系的に学ぶという意味でも入門書を買おうかなと思い、Amazonで「aws 入門書」を検索しました。業界トップは伊達ではなくて、そこには大量の入門書がありました。認定資格の対策本なんかも表示されていて、以前Twitterで取得した人を見かけたことを思い出し、なんとなく公式サイトのサンプル問題を覗いてみました。サービスだけでなく料金やサポートに関しての問もあり、過去問を解きながらそれに関係する内容を調べていくことで一般的な知識の習得が可能なように思いました。上手くいけば認定も取れて一石二鳥です。

認定は基礎から専門知識まで4つのレベルに分かれており、全部で12種類あります。当然最も簡単なクラウドラクティショナーをもとに勉強を始めることにしましたが、すぐに過去問が公開されていないことを知りました。サードパーティから模試が販売されており、価格も1500円程度と入門書より安価であるためそれを使って勉強することにしました。問題と選択肢に出てくる用語をひとつずつ検索し、公式サイトの概要を読み、個人や企業のブログをいくつか読んだりを繰り返して、メジャーなサービスや組み合わせくらいは学ぶことができました。試験を受けたところ合格できたので、ソリューションアーキテクトアソシエイトという認定の模試を使って同じように勉強しました。問われる内容が少し深くなるため、公式サイトでは開発者ドキュメントを読んで学びました。試験は合格しましたが、模試よりも難しくてより実践的な学習の必要性を感じました。

3月が始まって2週間程度経ち、入門はできたかなと思います。知らなかったことを知ると楽しくなってくるもので、いつの間にかAWSが好きになっていました。仕事でも使ってみたいなあ。