PRがマージされたら他のPRのブランチを更新するGitHub action

GitHubのPRがマージされたら、リポジトリの他のPRのブランチを更新するコマンドとそのGitHub actionを作った。

github.com

github.com

実行例

name: pru
on:
  push:
    branches:
      - main
    paths:
      - "**/*.go"
      - Makefile
permissions:
  contents: write
  pull-requests: write
jobs:
  pru:
    name: pru
    runs-on: ubuntu-latest
    steps:
      - uses: winebarrel/pru-action@v0.3.0
        with:
          paths: |
            **/*.go
            Makefile

この例だと、PR#1をマージしたときに、**/*.go,Makefileを含むPR#2のブランチを更新している。

仕組み

QuetaroというSQS+PostgreSQL+Lambdaのジョブキューシステムを作った

github.com

これは何?

SQS+PostgreSQL+Lambdaとその間をつなぐデーモンで構成されたジョブキューシステムです。 入り口のSQLにメッセージを投げると、メッセージで指定した関数名のLambdaが実行されます。 Lambdaの実行に失敗した場合は、適当な感覚でリトライされます。

Ruby製のジョブキューシステムQueとそのクローンであるqgを模した作りになっています。

モチベーションとか

コンテナ環境で使いやすいジョブキューシステムが欲しかったのが、モチベーションです。

Resqueのようにプロセスとしてジョブを起動するタイプだと、コンテナのライフサイクルとのミスマッチでスケールイン・スケールアウトが難しので、コンテナを使うならなるべくクラウド上の何か(ECS・Lambda…etc)でジョブを実行したいのですが、一方で完全にAWSリソースだけでジョブキューシステムを作ると、特にSQSの使い勝手が悪くキューの可観測性が低いので、もう少し使い勝手の良い何かが欲しいなーと。

アーキテクチャ

特徴

  • スケーラビリティとメンテナンシビリティを高くすることを目標に設計しています
  • 各デーモンはプロセスレベル・Goroutineレベルでスケールアウトできます
  • ジョブの実行は遅れますが各デーモンは停止可能です
    • たとえば「デーモンを一時的に停止してDBのアップグレードを行う」といったことが可能です
  • SQSのメッセージIDをDBのPK・構造化ログ・Lambdaのeventに出力しているので、メッセージIDでジョブの動作をトレースできます
  • ジョブキューとしてPostgreSQLを使っているので、SQSに比べて検索性が良いです

Getting Started

READMEに書いたとおりなんですが

1. docker-composeの起動

$ docker-compose up

2. terraformの適用

$ make tf-init
$ make tf-apply

※LocakStackに対してterraformを適用します

3. DBのセットアップ

$ make db

4. デーモンの再起動

初期状態ではデーモンが起動に失敗しているので、再起動します。

$ make restart


以上でサーバのセットアップは完了です。

最後にSQSにメッセージを投げるとLambdaが実行されることを確認できます。

$ make message

サンプルのLambdaはこんな感じです:


make failure を実行すると、Lambdaが失敗するメッセージをSQSに投げるので、ジョブがリトライする様子を確認できます。

$ make failure

DBに残っているジョブは http://localhost:8081 から確認できます。

QA

なんでLambdaなの?(Step Functions / ECSではないの?)

  • Lambda Destinationsが便利だったので…
  • SFn / ECSを実行しようとすると、AWSのリソースが増えてやや複雑になる/Destinationsのような処理を自前でやることになりそう
    • LambdaからDestinationsをなくした上でSFn / ECSをたたくようにして、SFn / ECSからsuccess/failureのキューにメッセージを投げるようにすることもできるかと
  • Lambdaだと15分の時間制限があるが、それを超えるようなジョブはバッチ処理に移譲してほしい気持ち
  • Dockerイメージが使えるのでECSとの併用はそんなに難しくはない…はず

クライアントから直接Lambdaを非同期で実行すればよいのでは?

  • Lambdaを非同期に実行するときに使われる内部的なキューが可視化されていないようなので、キューの状態を確認するのが難しそう
  • Lambdaの並列数上限に達してクライアントからのAPIコールが失敗するのが嫌

SQSからLambdaを実行すればよいのでは?

  • SQSの検索性が非常に悪いので、メッセージをSQSにためておきたくない
  • キューのメッセージの確認のために、ReceiveMessageでメッセージを受信して確認し、VisibilityTimeoutで再送を待つような破壊的な動作をやりたくない

DynamoDBを使えばよいのでは?

  • SKIP LOCKEDを使ったPostgreSQLのキューのように、排他制御を実装できるかわからなかった
  • PostgreSQLのかっちりとした型制約がほしかった

どこかのプロダクションで使ってるの?

  • まだです。そのうち突っ込みたい…

親に向かってなんだそのContextは。

親Contextの都合でcancelしてほしくないが、loggerの引き回しはContextでやりたい、みたいなときにDeadline()/Done()/Err()を無視すればいいのかなぁ…と思って実装してみた。

cancelされたくないというのは、SIGINTですべてのgoroutineを停止したいんだけど、goroutineのトランザクションは完了しててほしい、みたいな。

package ctxutil

import (
    "context"
    "time"
)

type withoutCancelCtx struct {
    parent context.Context
}

func (*withoutCancelCtx) Deadline() (deadline time.Time, ok bool) {
    return
}

func (*withoutCancelCtx) Done() <-chan struct{} {
    return nil
}

func (*withoutCancelCtx) Err() error {
    return nil
}

func (ctx *withoutCancelCtx) Value(key any) any {
    return ctx.parent.Value(key)
}

func WithoutCancel(parent context.Context) context.Context {
    return &withoutCancelCtx{parent}
}
   ctx = ctxutil.WithoutCancel(ctx)

ctxutil.WithoutCancel()を読んでいる関数の上位の関数では、親ContextのDone()をチェックして、子が終わったら終了する想定。

キャンセルされたくないトランザクションはいろいろあるきがしているけど、その対応の実装例をあまり見ないような気がしている。他の人、どうしているんだろう?

cronのスケジュールを表示する雑Webサービス作った

https://cronplan.in/

Show cron schedule.

USAGE:

  curl cronplan.in -d '5 0 * * ? *'

  curl cronplan.in -G --data-urlencode 'e=5 0 * * ? *'

see http://cronplan.in?e=5+0+*+*+?+*
$ curl cronplan.in -d '5/3 0/3 ? * FRI *'
Fri, 21 Oct 2022 00:05:00
Fri, 21 Oct 2022 00:08:00
Fri, 21 Oct 2022 00:11:00
Fri, 21 Oct 2022 00:14:00
Fri, 21 Oct 2022 00:17:00
Fri, 21 Oct 2022 00:20:00
Fri, 21 Oct 2022 00:23:00
Fri, 21 Oct 2022 00:26:00
Fri, 21 Oct 2022 00:29:00
Fri, 21 Oct 2022 00:32:00

EventBridgeのcron式のパーサーを作った話とDay of weekの増分の仕様

追記3

もろもろ作り直した

github.com

追記2

いろいろ間違っていた…

追記

1-10/3 とか MON-FRI/3 とかを忘れていたので作り直し…


cron式をパースするGolangのライブラリを書いた。

github.com

モチベーションとしては、Amazon EventBridgeのcron式を書くときに毎回JSTからUTCに変換するのがめんどくさくて「いつの日かterraformにユーザー定義関数が追加されるときに備えて(くるのか?)タイムゾーンをシフトするライブラリを書こう」という感じで始めた。

パーサー自体は alecthomas/participle でサクサク書けたが

github.com

時間をシフトする処理は難しくて諦めた。

たとえば 0 5-10 ? * FRI * のようなJSTのcron式をUTCに直すと 0 20-23 ? * THU *0 0-1 ? * FRI * の2つに分かれる。cron式の分解と合成のうまいやり方が思いつかなかった。自分が無知なだけで良い方法があるのかも


EventBridgeの仕様を参考にしたが、いくつか知らない仕様があって勉強になった。

たとえば

  • 0 0 ? * 3#2 *: 毎月の第二水曜日の00:00に実行
  • 0 0 8W * ? *: 毎月8日に一番近い平日に実行
    • 2022/10だと10/8は土曜日なので10/7に実行される
  • 0 0 L * ? *: 毎月の最終日の00:00に実行
  • 0 0 ? * L *: 毎週の最終日、つまり土曜日の00:00に実行

など

オラクルのドキュメントでも同様の記号が書かれていたので、AWSだけの仕様ではない…ような気がする(オラクルのほうのCの意味はわからなかった)

Day of weekの増分の仕様

時間のシフトを計算する処理は諦めたが、ASTだけあってもあまり有用ではなさそうなので、ついでとある時刻がcronのスケジュールにマッチするかどうか判別する機能も付けてみた。しかし曜日(Day of week)フィールドの処理で結構ハマってしまった…

増分の表現は、分子≒オフセット・分母≒増分と、分数のようなかたちで書く。(改めて書くほどのことでもないが)

  • 分・時フィールドの場合 */30/3 と同じで、実行される時間は 0 3 6 9 …となる。1/3の場合は1 4 7 10 …
  • 日(Day of month)フィールドだと、*/31/3 と同じ。実行される日付は 1 4 7 10 13…
  • さらに年フィールドの場合は、*/31970/3 と同じになる模様(AWSコンソール調べ)

曜日(Day of week)フィールドの場合、*/31/3 と同じ結果になる。

曜日の数値 1-7 は MON-SUN にマップされているので MON(1) THU(4) SUN(7)… となると思いきや、実はSUN始まりで SUM(0) WED(3) SAT(6)… となる。

曜日(Day of week)を考えないと、時刻をnとしたときに n % (分母) == (分子) という計算でその時刻がcronを実行する時刻なのか判別できるが、曜日(Day of week)は他と分子の扱いが違うので特別な処理を挟む必要があって、だいぶ実装に時間が取られてしまった。

これLinuxでも同様の動作なのかな

あと、EventBridgeだとMON-SUNが1-7に対して、プログラム上だとSUN-SATが0-6になっているので細々した変換が必要で、そちらもすこしめんどくさかった。