dorapon2000’s diary

忘備録的な。セキュリティとかネットワークすきです。

Twitter APIのPOST lists/members/create_allの制約と対応

突然の宣伝で恐縮ですが、最近ListIDOというサービスを作りました。

listido.dorapon2000.xyz

内部でTwitter APIを使っており、主にTwitterのリストを操作しています。その中で、リストにメンバーをまとめて追加できるPOST lists/members/create_allという大変便利なAPIがあります。このAPIがなければ、ListIDOというサービスを作ろうとは思いませんでした。

しかし、便利すぎるがゆえに、その制約がどうなっているのかが気がかりでした。フォローは1日に400人までしかできません。ではリストにメンバーを追加する場合は?制約の厳しさ次第でサービスの根幹を揺るがされてしまいます。

今回はそのAPIについて調べたことをまとめ、その制約に対する自分の対応も紹介します。

3行まとめ

  • 1度の呼び出しで100人まで追加できる
  • リストに大量・無差別なメンバーの追加はTwitterルールで禁止されている
  • 具体的なレートリミットはなさそう

POST lists/members/create_all

POST lists/members/create_all | Docs | Twitter Developer Platform

Twitterのリストにメンバーを複数人追加するAPIです。リストの作成や、1人だけ追加するAPIは別であります。また、API v2には同じ役割のAPIがないため、API v1.1でしかできない操作になります。

APIドキュメントに記載されている制約

  • メンバーの追加先のリストは自分が所有している必要がある
  • 1つのリストには5000メンバーまでしか含めない
  • 1度の操作で100人までしかリストに追加できない
  • 同じリストに対してメンバーの追加と削除を急激に行うと問題が起きる可能性がある
  • レートリミットあり

2つ目の5000メンバーという上限はリスト機能にあるそもそもの制約1です。3つ目の100人までしか一度に追加できないのはAPIを呼び出すときに注意が必要です。つまり、300人追加したい場合は3回に分けてAPIを呼ぶ必要があります。4つ目は成約というより注意喚起で、おそらくDBの整合性の問題なのだと思います。

問題は5つ目で、原文は

f:id:dorapon2000:20211115234433p:plain

YES!!!(具体的にいくつやねん!!!!!)これはPOSTのリスト操作APIすべてに言えます。

GETのAPIには何分間に何回までしか呼び出せない等の具体的なレートリミットが書かれているところに、POSTではただ「Yes」としか書かれていません。

Standard API v1.1の制約

Rate limits: Standard v1.1 | Docs | Twitter Developer Platform

こちらにおそらくすべてのGETのAPIと一部のPOSTのAPIのレートリミットが列挙されています。なぜPOSTは一部しか列挙されていないのか.... POST lists/members/create_allはもちろんありません。

Twitterの自動化ルール

Twitterの自動化開発ルール | Twitterヘルプ

少しずれますが、開発者向けの自動化ルールのページにも、リストに関する言及があります。

リストまたはコレクションへの追加の自動化: Twitterアカウントを大量または無差別にリストに追加したり、ツイートを大量または無差別にコレクションに追加したりしないでください。多数の無関係の利用者をリストに追加することは、Twitterルールで禁止されています。

リストに大量にアカウントを追加する行為はTwitterルールで禁止されているようです。この「大量」という言葉が曖昧ですが、Yesよりは具体的になりました。

API呼び出しのHTTPヘッダを見る

Twitter APIを呼び出すと、HTTPヘッダ内にレートリミットに応じた残りリクエスト数などが記載されます。

以下のサイトでは、実際にAPIを呼んだときのレスポンスヘッダまで載っているので、どうなるか見てみます。

POST lists/members/create_all - リストに複数のメンバーを追加する

f:id:dorapon2000:20211116001914p:plain

対照としてGETも

GET lists/show - リストを取得する

f:id:dorapon2000:20211116002118p:plain

GET lists/showにはx-rate-limit-limit/x-rate-limit-remaining/x-rate-limit-resetがありますが、POST lists/members/create_allにはありません。

困りました。

GET application/rate_limit_statusを使う

Retrieve rate limit details programatically | Docs | Twitter Developer Platform

Twitter APIの中にはレートリミットの残量を確認できるAPIがあります。これで調べられるのでは、と思ったところ

for read-only (GET) operations

GETしかわかりませんでした汗。

他の実装を見てみる

そもそも、なぜレートリミットが知りたいかというと、それによって実装が変わってくるからです。緩いなら連続でAPIを呼んでも問題にならないですし、厳しければ間隔を明けてAPIを呼ぶ必要があります。

https://github.com/search?l=Python&q=lists%2Fmembers%2Fcreate_all&type=Code

GitHub全体で検索して、他の実装がどうなっているのか調べてみました。流石に個人のコードをここに貼ったりするのははばかられるので、見た感じの感想を述べると、連続で呼び出す実装ばかりで、そこのところはあまり気にしていないようです。

これで1つ、実装の目安が立ちました。

レートリミットの答えらしきもの

blog.ryotak.me

ネットの海を探して、このような記事を見つけました。記事単体で面白かったのですが、注目したいのは以下の記述です。

Twitter APIの一部のレートリミットが、ドキュメント上には存在すると書いてあるにも関わらず、実際には存在しないことがわかった。 それらのエンドポイントの一つにPOST /1.1/lists/destroy.jsonが存在した。

薄々そう思っていましたが、おそらくPOST lists/members/create_allについてもそういうことなのでしょう。

ListIDOではどう実装したか

具体的な制約があれば、その制約を守っていれば怒られる(垢バン)ことはないです。しかし今回、その制約がなさそうということで、守るべきなのは制約ではなく節度に変わりました。

その節度も「大量の追加」という言葉で具体性がありません。結局、ListIDOでは1つのリストに対して15分に50人までしか追加できないようにしています。具体的には、ジョブサーバを立てて、リストへの追加作業を15分間隔でメインのアプリとは非同期に定期実行させます。300人のメンバーをリストに追加する必要があるならば、6つのジョブを75分かけて完遂させます。

単にfor文を回して3回APIを呼ぶ実装と比べて、格段に実装コストがあがりましたが、勉強になるからと自分をなだめました。

最後に

多くはいないが同じような疑問を持つ人は必ずいると思い、ニッチな内容ですが記事にしました。それで本記事を呼んで疑問を解消できたのであれば書いた甲斐があります。読んでいただきありがとうございました。

参考

pythonで例外クラスを引数にとる例外処理デコレータ

pythonの例外処理のプラクティスとして、例外処理デコレータを作るというやり方を最近知りました。更に発展させて、デコレータがどの例外の例外処理をするか選びたいとも思ってググりましたが、見つからなかったので、記事に残します。

一般的な例外処理

try-exceptを使った一般的な例外処理です。

class Exception1(BaseException):
    pass


def may_raise_exception():
    raise Exception1


def myfunc():
    try:
        may_raise_exception()
    except Exception1 as e:
        print('Exception1 occured!')

if __name__ == '__main__':
    myfunc()
$ python basic_exception.py
Exception1 occured!

デコレータを使った例外処理

自分が最近学んだやり方です。

import functools

class Exception0(BaseException):
    pass

class Exception1(BaseException):
    pass

def exception1(f):
    @functools.wraps(f)
    def wrapper(*args, **kwargs):
        try:
            return f(*args, **kwargs)
        except Exception1 as e:
            print('Exception1 occured!')
    return wrapper

def may_raise_exception():
    # raise Exception0
    raise Exception1

@exception1
def myfunc():
    may_raise_exception()

if __name__ == '__main__':
    myfunc()

myfuncでException1を発生したとき、デコレーター内のtry-exceptで処理して例外を握りつぶします。myfuncでException0が発生したときは、普通に例外が表示されます。

デコレータを使うことでtry-exceptをmyfuncの中に書かずに済み、見た目がスッキリしました。複数のメソッドで同じ例外処理をする必要があるときもデコレータをつけるだけなので便利です。

例外クラスを引数に取る例外処理デコレータ

上記の例外処理デコレータだけでも便利ですが、もう少し汎用性をもたせて、デコレータの引数にどの例外を受け取れるか指定したいと思い、実装しました。

import functools

# Exception0 〜 Exception3までの定義

def reraise(*catch_exceptions, reraise_exception):
    def decorated(f):
        @functools.wraps(f)
        def wrapped(*args, **kwargs):
            try:
                return f(*args, **kwargs)
            except catch_exceptions as e:
                print('may do something')
                raise raise_exception
        return wrapped
    return decorated


def may_raise_exception():
    raise Exception0
    # raise Exception1
    # raise Exception2


@reraise(Exception1, Exception2, reraise_exception=Exception3)
def myfunc():
    may_raise_exception()


if __name__ == '__main__':
    myfunc()

reraiseデコレータ(正確にはデコレータを返すreraise関数)はキャッチする複数個の例外クラスとリレイズする1つの例外クラスを引数に取ります。引数catch_exceptionsは可変長引数(*)なので、その後ろの引数reraise_exceptionはキーワード引数として渡す必要があります。

myfuncの動作としては、Exception0が発生したとき、reraiseの引数では指定していないためキャッチせず、そのままException0が発生します。myfuncでException1か2が発生したとき、reraiseデコレータの中でキャッチし代わりにException3が投げられます。

今回の例では引数で受け取った例外をリレイズしますが、except句の中身を変更することでそこはよしなに変更できます。

ここまでのコードはgithubにもあげています。

github.com

感想

ググって出なかったので自分が記事にしましたが、例外クラスを引数にとる例外処理デコレータはさすがにやりすぎなのかもしれないです。 普通、try句に入れるのは例外が発生する可能性のある必要最低限のコードです。例外処理デコレータはその関数すべてをtryで囲っているようなものなので、気をつけて使わないと範囲が広すぎます。

# OK
def myfunc() {
    # 何かしらの処理
    try:
        may_raise_exception()
    except Exception1:
        print('Exception1 occured!')
    # 何かしらの処理
}

# OK
@exception1
def myfunc() {
    may_raise_exception()
}

# tryの範囲が広い
@exception1
def myfunc() {
    # 何かしらの処理
    may_raise_exception()
    # 何かしらの処理
}

参考

VSCodeで「ターミナル プロセスが起動に失敗しました」

昨日までは普通にVSCode上でシェルが使えてたのに、突然起動に失敗したといってシェルが開かなくなってしまったので、その解決方法を残します。

環境

  • maxOS Big Sur 11.5.2
  • VSCode 1.59.1
  • fish

症状

VSCode上でターミナルを開こうとすると、あるいはデバッグを実行しようとして「ターミナル プロセスが起動に失敗しました: シェル実行可能ファイル "fish" へのパスが存在しません。」と表示されて何も起きません。おそらく、VSCodeを再起動したことで発生しました。

ターミナル プロセスが起動に失敗しました: シェル実行可能ファイル "fish" へのパスが存在しません。

解決方法

VSCodeのsettings.jsonを開いて、以下のように記述しました。VSCodeの再起動もする必要なく、設定後にシェルが正常に起動します。

    "terminal.integrated.defaultProfile.osx": "fish",
    "terminal.integrated.profiles.osx": {
        "fish": {
            "path": "/usr/local/bin/fish",
        },
    },

osxの部分には各OSに相当するものを入れます。

"fish"の2箇所はおそらくラベルで適当に入れました。 pathには自分が使いたいシェルへのパスを指定します。

原因

VSCode3月のアップデートでシェルのパスの指定方法が変わったことが原因のようでした。 そもそも自分はこれまでシェルのパス指定せずに使ってたので、今後は指定する必要が出てきたのかもしれないです。そうだとしたらちょっと不便ですね。。

code.visualstudio.com

terminal.integrated.defaultProfile.{platform}
terminal.integrated.profiles.{platform}

新しく追加された上記の設定でosごとに細かくシェルの設定ができるようになったみたいです。

参考

【VSCode】 "terminal.integrated.shell" is deprecated.【小ネタ】

VSCodeのターミナルの設定の詳細 - Qiita

macOSで1つのコアだけに負荷を掛けたいけどできない!

&とwaitに関する記事を書いた時、macで1つのコアだけに負荷を掛けたいという状況がありました。しかしどうしても均等に負荷が掛かってしまうので、そのことについてまとめます。なお、この記事からは、原因も解決策もわからないです。

宣伝

dorapon2000.hatenablog.com

目次

環境

やったこと

yesで負荷を掛ける

macOSに収録のyesコマンドはマルチスレッドに最適化されていないため、メニーコアのCPUを積むMacで実行しても、1基の論理CPU(コア)に対し負荷をかけるに過ぎないからだ。

ひたすらMacを重くする「ストレステスト」をお手軽に - 新・OS X ハッキング!(248) | マイナビニュース

私は上の記事を読んで、macのyesであれば1つのコアだけに負荷を掛けられると想像しました。(今あらためて読むと全体として1コア分の負荷を掛けるだけで、1つのコアとは言っていない気もしますが...)

❯ yes > /dev/null &
❯ ps -o pid,comm,%cpu
  PID COMM   %CPU
 6152 -fish   0.0
 8925 -fish   0.0
14652 yes   100.0

yesのcpu使用率は100%ですが、

f:id:dorapon2000:20210612183524p:plain:w400

yes > /dev/null & yes > /dev/null &

f:id:dorapon2000:20210612183555p:plain:w400

1つ実行でも2つ実行でも4つの物理コアに対して均等に負荷が掛かっています。

最近のmacだとなにか仕組みが違うのかと思って別のコマンドでも試してみました。

opensslで負荷を掛ける

yesで紹介した記事の方法にありました。こちらであればCPU数を指定できます。

openssl speed -multi 1

f:id:dorapon2000:20210612184449p:plain:w400

うーん、こちらでも均等に負荷が掛かってしまいます。

openssl speed -multi 8

f:id:dorapon2000:20210612184438p:plain:w400

ちなみに、8を指定すればすべての論理コアに負荷がかかることは確認できます。

stress-ngで負荷を掛ける

stress-ng — Homebrew Formulae

このコマンドであれば、指定したコアにだけ負荷を掛けられるようなので試してみます。

❯ brew install stress-ng

# CPU数1つでコア番号1に指定して負荷を掛ける
❯ stress-ng -c 1 --taskset 1
taskset: setting CPU affinity not supported

エラーが出ました。

結論っぽいもの

エラーについて調べると以下のような記事を見つけました。

stackoverflow.com

どうやら、macAPIではコアを指定してプロセスを実行できないようで、カーネルがすべてのスレッドを掌握しているようです。

となると、それぞれのコアに均等に負荷がかかるのはmacOSがよしなにした結果で、macでは1つのコアだけに負荷を掛けるのは無理そう?

これ以上はブロックボックスっぽいので調べられなさそうです。記事おわり!