@kyanny's blog

My thoughts, my life. Views/opinions are my own.

https://github.com/throttled/throttled と同じ GCRA アルゴリズムを実装(移植)した https://github.com/viafintech/gcra-ruby/tree/master には正式な mem_store 実装はないけどテストコードの中に実質 mem_store な実装がある。しかしそれを使ってレートリミットを実装してみると、時間が経過しても回復しない。

class TestStore
  attr_accessor :now
  attr_accessor :fail_sets

  def initialize
    @now = 0
    @data = {}
    @fail_sets = false
  end

  def get_with_time(key)
    return @data[key], @now
  end

  def set_if_not_exists_with_ttl(key, value, ttl)
    return false if fail_sets

    if @data.has_key?(key)
      return false
    end

    @data[key] = value
    return true
  end

  def compare_and_set_with_ttl(key, old_value, new_value, ttl)
    return false if fail_sets

    if @data[key] != old_value
      return false
    end

    @data[key] = new_value
    return true
  end
end
#!/usr/bin/env ruby

require 'gcra/rate_limiter'
require_relative 'test_store'

store = TestStore.new

rate_period = 0.5  # Two requests per second
max_burst = 10     # Allow 10 additional requests as a burst
limiter = GCRA::RateLimiter.new(store, rate_period, max_burst)

p limiter

x = 0
loop do
  exceeded, info = limiter.limit('foo', 1)
  pp [exceeded, info]
  if exceeded
    x += 1
  end
  if x > 50
    break
  end
  sleep(rand(0.01..0.02))
end
❯ ruby mem.rb
#<GCRA::RateLimiter:0x000000010a8091d8 @store=#<TestStore:0x000000010a8092a0 @now=0, @data={}, @fail_sets=false>, @emission_interval=500000000, @delay_variation_tolerance=5500000000, @limit=11>
[false, #<struct GCRA::RateLimitInfo limit=11, remaining=10, reset_after=0.5, retry_after=nil>]
[false, #<struct GCRA::RateLimitInfo limit=11, remaining=9, reset_after=1.0, retry_after=nil>]
[false, #<struct GCRA::RateLimitInfo limit=11, remaining=8, reset_after=1.5, retry_after=nil>]
[false, #<struct GCRA::RateLimitInfo limit=11, remaining=7, reset_after=2.0, retry_after=nil>]
[false, #<struct GCRA::RateLimitInfo limit=11, remaining=6, reset_after=2.5, retry_after=nil>]
[false, #<struct GCRA::RateLimitInfo limit=11, remaining=5, reset_after=3.0, retry_after=nil>]
[false, #<struct GCRA::RateLimitInfo limit=11, remaining=4, reset_after=3.5, retry_after=nil>]
[false, #<struct GCRA::RateLimitInfo limit=11, remaining=3, reset_after=4.0, retry_after=nil>]
[false, #<struct GCRA::RateLimitInfo limit=11, remaining=2, reset_after=4.5, retry_after=nil>]
[false, #<struct GCRA::RateLimitInfo limit=11, remaining=1, reset_after=5.0, retry_after=nil>]
[false, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=nil>]
[true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>]
[true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>]
[true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>]
[true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>]
[true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>]
[true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>]
[true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>]
[true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>]
[true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>]
[true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>]
[true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>]
[true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>]
[true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>]
[true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>]
[true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>]
[true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>]
[true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>]
[true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>]
[true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>]
[true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>]
[true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>]
[true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>]
[true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>]
[true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>]
[true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>]
[true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>]
[true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>]
[true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>]
[true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>]
[true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>]
[true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>]
[true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>]
[true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>]
[true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>]
[true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>]
[true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>]
[true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>]
[true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>]
[true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>]
[true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>]
[true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>]
[true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>]
[true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>]
[true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>]
[true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>]
[true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>]
[true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>]
[true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>]
[true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>]
[true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>]
[true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>]

TestStore のなかで ttl を無視してるからだと思うが、不思議なのは Go 実装の方の mem_store も ttl は無視しているのに、それを使ったレートリミットの実装ではちゃんと回復するところ。

https://github.com/throttled/throttled/blob/master/store/memstore/memstore.go

↓このコードは GOでGCRAレートリミット - aptpod Tech Blog から拝借したものを改変した。

package main

import (
    "flag"
    "fmt"
    "log"
    "time"

    "github.com/throttled/throttled/v2"
    "github.com/throttled/throttled/v2/store/memstore"
)

func main() {
    var (
        maxRate  int
        burst    int
        interval time.Duration
    )

    flag.IntVar(&maxRate, "r", 100, "")
    flag.IntVar(&burst, "b", 2, "")
    flag.DurationVar(&interval, "i", time.Second, "")

    flag.Parse()

    store, err := memstore.New(65536)
    if err != nil {
        log.Fatal(err)
    }

    quota := throttled.RateQuota{
        MaxRate:  throttled.PerSec(maxRate),
        MaxBurst: burst,
    }
    rateLimiter, err := throttled.NewGCRARateLimiter(store, quota)
    if err != nil {
        log.Fatal(err)
    }

    ticker := time.NewTicker(interval / 100)
    defer ticker.Stop()

    for i := 0; i < 100; i++ {
        ng, res, err := rateLimiter.RateLimit("sample", 1)
        if err != nil {
            log.Fatal(err)
        }
        if ng {
            fmt.Printf("NG res = %+v\n", res)
            continue
        }
        fmt.Printf("OK res = %+v\n", res)
    }
}
go mod init
go mod tidy
go run main.go
❯ go run main.go | head
OK res = {Limit:3 Remaining:2 ResetAfter:10ms RetryAfter:-1ns}
OK res = {Limit:3 Remaining:1 ResetAfter:19.95ms RetryAfter:-1ns}
OK res = {Limit:3 Remaining:0 ResetAfter:29.947ms RetryAfter:-1ns}
NG res = {Limit:3 Remaining:0 ResetAfter:29.942ms RetryAfter:9.942ms}
NG res = {Limit:3 Remaining:0 ResetAfter:29.94ms RetryAfter:9.94ms}
NG res = {Limit:3 Remaining:0 ResetAfter:29.938ms RetryAfter:9.938ms}
NG res = {Limit:3 Remaining:0 ResetAfter:29.936ms RetryAfter:9.936ms}
NG res = {Limit:3 Remaining:0 ResetAfter:29.934ms RetryAfter:9.934ms}
NG res = {Limit:3 Remaining:0 ResetAfter:29.932ms RetryAfter:9.932ms}
NG res = {Limit:3 Remaining:0 ResetAfter:29.931ms RetryAfter:9.931ms}

こちらは一応ちゃんと ResetAfter と RetryAfter の値が変化しているのでちゃんと動いているように見える。

どこに違いがあるのか、Go の mem_store のどこが Ruby の TestStore と意味的に違うのかがわからない。

TOEIC S&W 320

TOEIC score speaking 140 writing 180 total 320
TOEIC score speaking 140 writing 180 total 320

七月に受けた TOEIC L&R に続き、九月に受けた TOEIC S&W の結果が出た。Speaking 140 Writing 180 Total 320。目標スコアは S 150 W 150 T 300 だったので、合計では上回ったが Speaking が届かなかった。

Speaking が予想より低かったのがショックで、ここ数日落ち込んでいる。英語学習へのモチベーションも下がってしまった。オンライン英会話を二年やってまだこのレベル、しかも始めた当初と比べると日々の成長実感はほとんど感じなくなっていて、このままだとどれだけ続けてもこれ以上上達しないのではないか、と暗鬱な気持ちになる。IIBC AWARD OF EXCELLENCE の受賞基準にも Speaking だけ届かなかった。Speaking のテストを受験するのは開催地の関係でコスト高なので、次のチャンスは早くても一年後。あと一年も「届かなかった」と思いながら過ごすのかと思うとさらに気が滅入る。

デジタル公式認定証

TOEIC S&amp;W デジタル公式認定証
TOEIC S&W デジタル公式認定証

GitHub Pages のカスタムドメイン用の TLS 証明書は Let's Encrypt

docs.github.com

カスタムドメインを追加する前の、ユニークなサブドメインで公開されてる HTTPS なサイトは DigiCert の証明書を使っている。

例: https://automatic-adventure-k6ywp91.pages.github.io/

❯ echo | openssl s_client -connect "automatic-adventure-k6ywp91.pages.github.io:443" | awk '/BEGIN/,/END/' | openssl x509 -noout -issuer -subject -ext subjectAltName
depth=2 C = US, O = DigiCert Inc, OU = www.digicert.com, CN = DigiCert Global Root CA
verify return:1
depth=1 C = US, O = DigiCert Inc, CN = DigiCert TLS RSA SHA256 2020 CA1
verify return:1
depth=0 C = US, ST = California, L = San Francisco, O = "GitHub, Inc.", CN = *.pages.github.io
verify return:1
DONE
issuer=C = US, O = DigiCert Inc, CN = DigiCert TLS RSA SHA256 2020 CA1
subject=C = US, ST = California, L = San Francisco, O = "GitHub, Inc.", CN = *.pages.github.io
X509v3 Subject Alternative Name:
    DNS:*.pages.github.io, DNS:pages.github.io

カスタムドメインを登録して検証が完了すると、カスタムドメイン用の証明書が発行されて置き換わる。この挙動は証明書プロビジョニングのトラブルシューティング ("Certificate not yet created" (証明書がまだ作成されていません) エラー)で説明されている。

証明書の発行処理中は Web インターフェースにこんなメッセージが表示される。2/3 は一瞬すぎて見えなかったので、どんなメッセージなのかは不明。

証明書が実際に適用されるまでの間は、サイトのホスト名(ドメイン)と証明書の内容が一致しないので当然エラーになる。

例: https://expert-octo-palm-tree.kyanny.work/

❯ echo | openssl s_client -connect "expert-octo-palm-tree.kyanny.work:443" | awk '/BEGIN/,/END/' | openssl x509 -noout -issuer -subject -ext subjectAltName
depth=2 C = US, O = DigiCert Inc, OU = www.digicert.com, CN = DigiCert Global Root CA
verify return:1
depth=1 C = US, O = DigiCert Inc, CN = DigiCert TLS RSA SHA256 2020 CA1
verify return:1
depth=0 C = US, ST = California, L = San Francisco, O = "GitHub, Inc.", CN = *.github.io
verify return:1
DONE
issuer=C = US, O = DigiCert Inc, CN = DigiCert TLS RSA SHA256 2020 CA1
subject=C = US, ST = California, L = San Francisco, O = "GitHub, Inc.", CN = *.github.io
X509v3 Subject Alternative Name:
    DNS:*.github.io, DNS:github.io, DNS:*.github.com, DNS:github.com, DNS:www.github.com, DNS:*.githubusercontent.com, DNS:githubusercontent.com
❯ curl -v https://expert-octo-palm-tree.kyanny.work/
,*   Trying 185.199.111.153:443...
,* Connected to expert-octo-palm-tree.kyanny.work (185.199.111.153) port 443 (#0)
,* ALPN: offers h2,http/1.1
,* (304) (OUT), TLS handshake, Client hello (1):
,*  CAfile: /etc/ssl/cert.pem
,*  CApath: none
,* (304) (IN), TLS handshake, Server hello (2):
,* (304) (IN), TLS handshake, Unknown (8):
,* (304) (IN), TLS handshake, Certificate (11):
,* (304) (IN), TLS handshake, CERT verify (15):
,* (304) (IN), TLS handshake, Finished (20):
,* (304) (OUT), TLS handshake, Finished (20):
,* SSL connection using TLSv1.3 / AEAD-AES256-GCM-SHA384
,* ALPN: server accepted h2
,* Server certificate:
,*  subject: C=US; ST=California; L=San Francisco; O=GitHub, Inc.; CN=*.github.io
,*  start date: Feb 21 00:00:00 2023 GMT
,*  expire date: Mar 20 23:59:59 2024 GMT
,*  subjectAltName does not match expert-octo-palm-tree.kyanny.work
,* SSL: no alternative certificate subject name matches target host name 'expert-octo-palm-tree.kyanny.work'
,* Closing connection 0
curl: (60) SSL: no alternative certificate subject name matches target host name 'expert-octo-palm-tree.kyanny.work'
More details here: https://curl.se/docs/sslcerts.html

curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the web page mentioned above.

数分経つと証明書が入れ替わって、普通にアクセスできるようになる。

❯ echo | openssl s_client -connect "expert-octo-palm-tree.kyanny.work:443" | awk '/BEGIN/,/END/' | openssl x509 -noout -issuer -subject -ext subjectAltName
depth=2 C = US, O = Internet Security Research Group, CN = ISRG Root X1
verify return:1
depth=1 C = US, O = Let's Encrypt, CN = R3
verify return:1
depth=0 CN = expert-octo-palm-tree.kyanny.work
verify return:1
DONE
issuer=C = US, O = Let's Encrypt, CN = R3
subject=CN = expert-octo-palm-tree.kyanny.work
X509v3 Subject Alternative Name:
    DNS:expert-octo-palm-tree.kyanny.work

夢日記

オフィスというか雑誌の編集部のような雑然とした大きな部屋で働いている。時計は 23 時を回っている。時間が時間なので人影はまばらで、おれを含めて四、五人といったところか。おれはパソコンに向かって一生懸命何かを書いている。一区切りついたところで時計を見ると 25 時を回っている。25 時だって?いつの間に二時間も経ったんだろう。もう電車もないし、歩いて帰るしかない。

おれは帰り支度を始める。机の上のゴミを片付けなければ。流しには他の人たちが放置していったカップラーメンの容器がいくつもある。ついでに片していくか。多少遅くなったって構やしない、どうせ電車はもうないのだ。カップラーメンの容器やら何やらを両手に抱えて自席を離れるそのとき、机の隅に別のカップラーメンの容器を見つけてしまった。まだスープが残っていて、ふやけた麺も見える。参ったな、一体いつのだ?他人のゴミの後始末をする気が一気に失せる。

誰かが「次のマネージャーは、いま仕切ってるあの人じゃなくて I さんになるらしいよ」と話しているのが聞こえる。おれに言ってるのか?I さんだって?何年か前におれが雇った人だ。マネージャーの仕事はあまり好きでないといって転職したんじゃなかったっけ。いつの間にか立場が逆転してしまったのか。彼はなかなか腕が立つ人だ。

校舎と校舎の間のちょっとした通路みたいなところを歩いている。雪が降って数センチ積もったあとで、シャーベット状になった茶色い水たまりがあって歩きづらい。隣で上級管理職の S さんが、君の能力は買っている、とかいずれは技術戦略を任せたいと思っている、とか話している。おれは間に受けず、警戒している。この人の言うことはあまり信用できない。リップサービスなのが見え見えだ。I さんはどうしたんだろう?


おれは疲れているんだと思う。身体がなんとなくだるいし、気持ちも落ち込んでいる。小説を読みたい気分だ。落ち込むと小説を読みたくなる。殺風景だったり不条理だったりする、冷たくて荒涼とした文章を。心がそういうものを求めている。疲れているときはいつもこうだ。まだいいほうで、もっと悪化して気持ちが塞ぎ込むことが続くと、ソースコードをノートに手で模写したり、非生産的なことに没頭して無作為な時間を過ごすようになる。そこまでいってないだけまだマシだ。あれは悲しいものだ。

Bizmates Program: Level 4 Rank C Lesson 8: Constant versus Seasonal

ミスアンミカと。「こんばんは!何かニュースはありますか?」えーと、今月上旬に TOEIC の Speaking & Writing テストを受けて、昨日結果が来たんだけど、スピーキングのスコアが思ったよりちょっと低くて、ちょっと気分が落ちてます。「語学学習は上がったり下がったりだけど、努力したことは認めなきゃ。それを糧に次頑張ればいいわよ!」みたいなことを言って励ましてくれた気がする。「猫ちゃんはどうしてる?」ちょうど今日会社の猫好きチャンネルで、overgrooming で下半身の毛が無い猫に悩んでる話が出て、「下半身の毛がない」という英語表現をみたばかりなのだが覚えてなくて言えず、仕方なくスマホで今日撮った三毛猫の写真を見せて場を繋いだ。ムッとした顔の写真だったので「彼は怒ってるのかしら?写真撮ってるから?」彼女だけどね、多分、許可した覚えないけど?って抗議してるのかもね、などと。

Lesson 8 の See 読んだ後の質疑応答から。「あなたの仕事はどっち?」ほぼ完全に constant ですね。「seasonal な仕事に就きたいと思う?」うーん、正直経験ないのでどういう仕事かイメージ湧いてないけど、思いつくのは漁師とか農家とかで、そういう仕事ってきつそうだからなあ。ピークの時はものすごく忙しいだろうし、逆にオフの時期とか、仕事したくでもできない時期、漁師なら台風で海が荒れてる時は漁に出れない。その間収入もない。どっちにしろ extreme ですよね、そういうのはストレスが強そうだから、constant の方がいいかな。あなたはどう?「私?constant がいいわね、家族がいるし。(昼の)仕事をやめようと思ってるのもそれが理由よ。今の仕事は支社への出張があって、一週間二週間とか家を空けることになるので。ビズメイツならどこからでも働けるし、時間も自由。上司もいない」

Try にうつり、1, seasonal な仕事は?さっきも言ったけど、漁師、農家、あとは・・役者かな?舞台や映画の撮影中、その前の稽古の時は忙しいけど、毎日毎日演じてるわけじゃないよね。仕事の時期が終わったら長めの休暇を取ったりするはず。「他には?」思いつかないなあ、なにかある?「リゾート地で働く人とかね」あー、それで思い出した、アマゾンの倉庫で働く人。ホリデーシーズンとかにパートタイムでたくさん雇われる。2, もし仕事で何ヶ月か家を離れることになったら家族はどんな反応する?そんな仕事辞めれば?って言いそう。でも、もし僕がその仕事を本当にやりたくて、やると心に決めているなら、最後は認めてくれると思う。ただ、もし会社・上司の命令で仕方なく、という感じなら、他の仕事を探すことを応援してくれるかもね。3, seasonal occupation に求められる personality は?flexible で open-minded なこと、かな。seasonal だと突然イベントが発生するだろうから、そういうのを楽しめて臨機応変に対応できる人があってそう。4, その他の pros/cons は?constant の pros は、さっきも話したけど stable ということ。これは、仕事の時間が安定してて読みやすいから休暇のスケジュールや一日のスケジュールを決めやすいというのもあるし、収入の面でも安定しているから、今年・来年・今後いくら稼げるか予想できる。seasonal は、たとえば農家なら台風で作物が全滅したらその年の収入はゼロ。台風が来るかどうかなんて予想できない。こういうのは大きな cons の一つ。constant の cons は、仕事に飽きやすいこと。そして短期的には昨日と同じ仕事を同じようにするだけでやっていけるけど長期的にはそれだとスキルや知識が陳腐化して ChatGPT に置き換えられてしまう。と言ったら笑ってくれて、狙ったウケを取れてよかった。

ここでレッスン終了。Lesson 8 の Try まで終わり、次は Act から。