連絡がつくもの・つかないもの
1日1回以上確認する
- Twitter: @const_MUTE
- タイムラインはほぼ確認できていません
- DMは通知が来るように設定しており、確認しています
数日に一度確認する
- Discord
未定
- Bluesky: @vraisamis.bsky.social
- 文字通りアカウントを「作っただけ」
- Misskey: @vraisamis
- 文字通りアカウントを「作っただけ」
上記を読んで、先日つくったGraphQLアプリに対策を施すことができるか確認した。
この2つはメソッドを呼ぶだけで対策が可能。
SchemaBuilder
型のメソッドに disable_introspection()
disable_suggestions()
があるので、それらを呼ぶだけ。
例えば、releaseビルドでのみ無効化したいときは、以下のようにする。
let s: SchemaBuilder<_, _, _> = ...; if cfg!(not(debug_assertions)) { s = s.disable_suggestions().disable_introspection(); }
元々対策されている模様。
(……が、エラーメッセージが「再帰深度の最大は32です」なので確認したほうがよさそう)
query { user(id: "user-01HBCCGK3MG5HA7GJG25BGV6PJ") { ...A } } fragment A on User { name ...B } fragment B on User { ...A }
{ "data": null, "errors": [ { "message": "The recursion depth of the query cannot be greater than `32`", "locations": [ { "line": 12, "column": 20 } ] } ] }
aliasはどれだけ多くても許可するようになっている。以下のように10000個のaliasをリクエストしたが普通にresponseが返ってきた。
{ alias0: user(id: "user-01HBCCGK3MG5HA7GJG25BGV6PJ") { name } alias1: user(id: "user-01HBCCGK3MG5HA7GJG25BGV6PJ") { name } # ... alias9999: user(id: "user-01HBCCGK3MG5HA7GJG25BGV6PJ") { name } }
これに対策するには独自のExtensionの実装が必要。以下に記載がある。
この中の parse_query
を利用する。
let result = next.run(ctx, query, variables).await?;
とすると、resultの中からパース済みのGraphQL queryを取り出せるので、そこからaliasを見つけてカウントしていき、エラーを出すようにする。
今回は以下のように制限した。
全体コードは以下。
実装のコアは以下。
#[async_trait] impl Extension for RestrictQueryAliasesImpl { async fn parse_query( &self, ctx: &ExtensionContext<'_>, query: &str, variables: &Variables, next: NextParseQuery<'_>, ) -> ServerResult<ExecutableDocument> { let result = next.run(ctx, query, variables).await?; // after parsed let query_selection_set = find_query(&result); let aliases = alias_count_recursive(&query_selection_set); if aliases > self.limit { Err(ServerError::new( format!("エイリアスは全部で{}個より多くできません", self.limit), None, ))?; } let aliases_per_level = alias_count_by_nested_level_recursive(&query_selection_set, 1); let max_aliases_per_level = max_value_or(aliases_per_level, 0); if max_aliases_per_level > self.limit_per_level { Err(ServerError::new( format!( "エイリアスは各階層で{}個より多くできません", self.limit_per_level ), None, ))?; } Ok(result) } }
このextensionをSchemaBuilderで s.extension(RestrictQueryAliases::default())
として作成すればOK。
同じ階層に4つ以上aliasをいれてリクエストをすると以下のようにエラーになる。
{ alias0: user(id: "user-01HBCCGK3MG5HA7GJG25BGV6PJ") { name } alias1: user(id: "user-01HBCCGK3MG5HA7GJG25BGV6PJ") { name } alias2: user(id: "user-01HBCCGK3MG5HA7GJG25BGV6PJ") { name } alias3: user(id: "user-01HBCCGK3MG5HA7GJG25BGV6PJ") { n0: name n1: name n2: name ownedBoards { b0: id b1: id b2: id owner { id } } } }
{ "data": null, "errors": [ { "message": "エイリアスは各階層で3個より多くできません" } ] }
同じ階層ではaliasを3つ以内に押さえていても、全体で10個以上になるリクエストは以下のようにエラーになる。
{ alias0: user(id: "user-01HBCCGK3MG5HA7GJG25BGV6PJ") { name } alias1: user(id: "user-01HBCCGK3MG5HA7GJG25BGV6PJ") { name } alias3: user(id: "user-01HBCCGK3MG5HA7GJG25BGV6PJ") { n0: name n1: name n2: name ownedBoards { b0: id b1: id b2: id owner { id i0: id i1: id } } } }
{ "data": null, "errors": [ { "message": "エイリアスは全部で10個より多くできません" } ] }
RustでGraphQLアプリを構築して、気づいたことや感想まとめ
https://github.com/vraisamis/graphql-rust-sample
だいたいこんな。未実装箇所もある https://github.com/vraisamis/graphql-rust-sample#domain
だいたいこんな。 https://github.com/vraisamis/graphql-rust-sample#layer
Rustの言語機能関連
shaku
のモジュール M
が Box<dyn UsersQuery>
をprovideできるとき、 M: HasProvider<dyn UsersQuery>
を満たしている。
M
の具体型は実装レイヤで決まるので、実装に関与しないレイヤでは抽象的に扱いたい。
しかしこのとき、 M
の指定が大変なことになりやすい。
fn foo<M>(m: &M) where M: HasProvider<dyn UsersQuery>, M: HasProvider<dyn BoardQuery>, M: HasProvider<dyn ColumnsQuery>, M: HasProvider<dyn CardsQuery>, // ... { todo!() }
これを解消するために直感的には type
でエイリアスを定義したい。
// これはできない type MyFuncTrait = HasProvider<dyn UsersQuery> + HasProvider<dyn BoardQuery> + HasProvider<dyn ColumnsQuery> + HasProvider<dyn CardsQuery>;
これはできない。
SEE: https://zenn.dev/ttttkkkkk/articles/e213d91ef7b47a
上の記事に全部書いてあるが……以下をやる。
// trait定義 pub trait QueryProvider where Self: HasProvider<dyn UsersQuery> + HasProvider<dyn BoardQuery> + HasProvider<dyn ColumnsQuery> + HasProvider<dyn CardsQuery>, { } // すべての前提traitを実装している型に対して全称実装 impl<T> QueryProvider for T where Self: HasProvider<dyn UsersQuery> + HasProvider<dyn BoardQuery> + HasProvider<dyn ColumnsQuery> + HasProvider<dyn CardsQuery>, { }
ちなみに、この用途だと FooQuery
が増えるたびに2箇所に追記していくことになる。マクロで解決できないかと思ったが、構文エラーになって駄目だった。
// これはできない macro_rules! queries { () => { HasProvider<dyn UsersQuery> + HasProvider<dyn BoardQuery> + HasProvider<dyn ColumnsQuery> + HasProvider<dyn CardsQuery> }; } pub trait QueryProvider where Self: queries!(), { } impl<T> QueryProvider for T where Self: queries!(), { }
pub(crate)
とか re-exportとかRustのstruct, enum定義は縦に長くなりがちである。これは主に以下のような理由による。
Foo<T>
)場合、 T
が実装しているtraitによって実装するメソッドを増やしたりするともっと多くなるimpl FooTrait for Foo where ...
がいっぱいになるまた、DDDでは以下のようにしたくなる。
User
UserName
Email
use domain_kanban::user::{User, UserName, Email}
これをそのまま実現しようとすると、1ファイルに大量の定義や実装を書くことになる。対策として、複数ファイルに分ける方法を利用できる。
具体的には、以下のようにuser/email.rs, user/user_name.rs
を作成する。
src/ |-- lib.rs |-- user/ | |-- email.rs | `-- user_name.rs `-- user.rs
そして、以下のようにする。
// -------- user/email.rs -------- // `pub(crate)` で(直接)参照可能範囲を制限する pub(crate) struct Email { // ... } impl Email { // ... } // -------- user/user_name.rs -------- pub(crate) struct UserName { // ... } impl Email { // ... } // -------- user.rs -------- // mod にはpubをつけない( `domain_kanban::user::email::Email` というふうには参照させない) mod email; mod user_name; // pub use (=re-export) で `domain_kanban::{Email, UserName}` で参照できるようにする pub use email::Email; pub use user_name::UserName; pub struct User { // ... } // ...
こうすることで、ファイルが長くなるのを抑えつつ、外部への見せ方を変える事ができる。 ただし、こうするとファイル数が多くなるので、やり過ぎは禁物。
SEE: https://qnighy.hatenablog.com/entry/2017/06/14/220000
Vec<T>
を map
していると、軽率に Vec<Result<T, E>>
になる。
これだと扱いにくいのだが、 collect
でどうにかできる。
let vec_of_results = vec![Some(1), Some(2)]; // collectは複数の型に変換できるので、要求する型を束縛する変数側に指定する // `collect::<Result<Vec<_>, _>()` でもいいが、個人的にはこちらのほうが書きやすい let result_of_vec: Result<Vec<_>, _> = vec_of_results.into_iter().collect(); // `?` が使える文脈なら、それを使って処理を続ける let v = result_of_vec?; // `?` を使うなら、collect側に書いたほうが1行で完結できる let v = vec_of_results.into_iter().collect::<Result<Vec<_>, _>>()?;
dataloader実装(後述)をやっていても、複数回クエリを回したいときもある。
let result_futures: Vec<impl Future<Output=Foo>> = keys .into_iter() // some_queryがFutureを返す .map(|k| some_query(/* ... */)) .collect();
このあとどうやって処理するかというと、futures-util join_all
を使う。
let result_vec: Vec<Foo> = join_all(result_futures).await;
オニオンアーキテクチャにおけるレイヤー分けを見据えて、リポジトリ全体はVirtual Workspaceにした。
ワークスペースのメンバcrateは、 crates/
ディレクトリ配下に置いた。rootディレクトリがゴチャつかないようにするためで、好みの問題かもしれない。
メンバcrateの指定は Cargo.toml
で以下のようにした。
[workspace] members = [ 'crates/*' ]
bin crateが crates/main
だけで他がlib crateであれば、 cargo run
で問題なくmainが起動できた。
https://doc.rust-lang.org/cargo/reference/workspaces.html#the-dependencies-table
Workspaceでこれが一番便利だったかもしれない。
/Cargo.toml
の workspace.dependencies
配下にいつも通り依存関係を書く。
各 /crates/*/Cargo.toml
の dependencies
で、 package-name.workspace = true
とすると、そのcrateを利用できる。
例:
# -------- on /Cargo.toml -------- [workspace.dependencies] anyhow = "1.0.72" # -------- on /crates/main/Cargo.toml -------- [dependencies] anyhow.workspace = true # -------- on /crates/domain-util/Cargo.toml -------- [dependencies] anyhow.workspace = true
実際に定義したものを列挙してコメントしておく。説明の都合sourceとは掲載順が違うことがある。
# -------- on /Cargo.toml -------- [workspace.dependencies] # anyhow, thiserror: エラー処理はどの層でも行うので共通で入れた # ちゃんとエラー処理するほうに倒すと、anyhowは減っていく可能性もある anyhow = "1.0.72" thiserror = "1.0.47" # tracing, tracing-subscriber: ロギング。 # ログに使っているcrate versionが揃うように。 # tracing-subscriberはもしかしたらmainだけでいいかもしれない。ちゃんとログ実装したら除去できるかも。 # 各層から依存するlog crateでtracingを使っていることを隠蔽しようかとも思ったけど、 # マクロの構文が物によって違いそうなのでやめた。 tracing = "0.1.37" tracing-subscriber = "0.3.17" # async-trait: infrastructure層で外部リソースを使用することが必至なため、だいたいasyncにする。 # I/Fはquery層などに置くことになるので、trait methodとしてasyncにする必要がある。 async-trait = "0.1.72" # futures-util, itertools: 一般的なutility futures-util = "0.3.28" itertools = "0.11.0" # shaku: DI。I/F側と実装側でバージョン統一するため。 shaku = "0.6.1" # sqlx: これはinfrastructure層でしか使わない……と見せかけて、migrateとかscriptとかで使っている(後述) # ここではバージョンのみ定義していて、featuresは使う側のcrateで決めている。 sqlx = "0.7.2" # ulid: これはdomain-utilで吸収しているのでここではいらないかも…… ulid = "1.0.1" # 以下各レイヤの定義。利用側でpathを毎回書かなくてよくなる。 # 依存されないcrateはここには列挙していない presentation-axum = { path = "./crates/presentation-axum" } presentation-graphql = { path = "./crates/presentation-graphql" } infrastructure-rdb = { path = "./crates/infrastructure-rdb" } query-resolver = { path = "./crates/query-resolver" } domain-kanban = { path = "./crates/domain-kanban" } domain-util = { path = "./crates/domain-util" } # tokio: main関数を使う場所で使っている。mainアプリケーションでは下層のspawnerにも使っている。 [workspace.dependencies.tokio] version = "1.29.1" features = ["macros", "rt-multi-thread"]
cargo-add
とかのツールのサポート(後述)それぞれ依存crate管理に関わるツール。ツール単体に気になるところはなく、普通に便利だった。
workspace dependencies を書くうえではコツがいりそうなので、ポイントに絞って書く。
端的に言うとworkspaceはサポートしていない。
issueはここにある: https://github.com/rust-lang/cargo/issues/10608
workspace dependenciesには自分で書くとして、そのヒントとなる情報は --dry-run
オプションで得るのがちょっと便利だった。
# --package オプションの指定がめんどくさいので、移動しておく。 $ cd crates/main $ cargo add frunk --dry-run Updating crates.io index Adding frunk v0.4.2 to dependencies. Features: + frunk_proc_macros + proc-macros + std + validated - serde warning: aborting add due to dry run
この結果を見ると以下がわかる。Webで調べる場合との比較も書いておく。
/Cargo.toml
に書いてないこともある。Virtual Workspaceを使っていてmulti crateそれぞれpublishされている場合とかfrunc_proc_macros, proc-macros, std, validated
で、optional featuresは serde
らしい
Cargo.toml
をみたらいいかわかりにくい。みてもわからないことも……validated, proc-macros, serde
が指定するもので、残りは依存されるものでおそらく外部から指定するものではないserde
は Cargo.toml
に載ってない。READMEにだけあるworkspace supportはあるっぽい。
w, --workspace
Cargo.toml
をみるみたいR, --root-deps-only
上記を整理すると、以下のような使い分けになる。
cargo outdated -Rw
cargo outdated -R
進むうちに、いくつかスクリプトが欲しくなった。
cargo ...
などのコマンド。オプションが多くて思い出すのがめんどくさくなってきたそれぞれ検討した方法は以下の通り。1番目に記載したものが採用したもの。
cargo-make
にまとめる
cargo-make
にまとめると cargo make migrate
みたくすることができるMakefile.toml
を書くのがやや面倒前述のように、bin crateを追加したことで実行時引数が増えた。
cargo run
cargo --project main run
ちょっと増えたぐらいだから別にいいんだけど、 default-members
に指定することで引数不要にできるのでそうした。
[workspace] members = [ 'crates/*' ] default-members = [ 'crates/main' ]
GraphQLサーバーのコードを更新するたびに再ビルドさせて実行させるのに、 cargo-watch
が便利だった。
cargo watch -x run
で実行できる。
watch
はデフォルトで check
をするので、 x
で実行内容を指定するdefault-members
でないものは -package
などを指定する必要があるが、 x 'check --package scripts'
のようにquoteが必要cargo watch -- cargo check --package scripts
のようにもできるらしいマクロをそれなりに使うときは、 cargo-expand
を使うとコンパイルエラー調査が便利である。
今回だと、async-trait, tokio, async-graphql, shaku, sqlxあたりがマクロをよく使う。
cargo-expand
はデフォルトだと全部を展開しようとする。itemの指定ができるので、以下のように使うと便利。
# 該当のcrate rootまで移動しておく cd crates/infrastructure-rdb # mod単位でexpand cargo expand query::card
あまり書くことはないけど使ったもの。
VecをgroupingしてHashMapにするなどのときに便利だった。 でもびっくりするぐらいメソッドがあるので、iterator関連のほしいメソッドはググるのが賢明。
future関連のメソッドが追加されていて便利。
どうしてもiteratorの要素ごとにFutureが生成されてしまったので、 join_all
を使った。
GraphiQLというGraphQLのUIがあり、 async-graphql
でもすぐ利用できるようになっている。
cargo watch -x run
で起動し、8000ポートにマップした。
GraphQLの要素が補完され、ドキュメントも出るのでクエリ組み立てに便利。
watch
でサーバーが再起動しても、UIは残るので使いやすかった。
サーバー再起動時にはschemaを取り直さないので、 Refresh GraphQL schema
ボタンを押すことで補完されるようになる。
dataloaderを実装した(というより、schemaのresolverを分けた)ことで、相互参照が実現できた。
{ usersAll { id name # userがオーナーのボードを参照 ownedBoards { id title # ボードのオーナーを参照 owner { id name } } } boardsAll { id title # ボードのオーナーを参照 owner { id name } } }
schemaのresolverを分けたりしなくても相互参照はできる。 しかし、dataloaderの実装に任せることで、実装をシンプルに保つことができた。
GraphQL実装はRust製のasync-graphqlを使った。一通りの機能があり、ドキュメントもあるのであまり困らず使えた。
ここにある通りに書いた。
実装上の考慮事項として、Schema型にはdataやextentionsなどのデータを紐づけているが、これはスキーマ生成だけを考えると不要である。そのため、以下のように考慮しておく(と楽)。
共通化にあたって、SchemaやBuilderの型引数にquery, mutation, subscriptionがあって煩雑になったので、 type
キーワードでエイリアスを作って対策した。
// 定義 type SchemaType = Schema<Query, EmptyMutation, EmptySubscription>; type SchemaBuilderType = SchemaBuilder<Query, EmptyMutation, EmptySubscription>; // 利用 fn schema_builder() -> SchemaBuilderType { Schema::build(Query, EmptyMutation, EmptySubscription) } fn schema() -> SchemaType { schema_builder().finish() } fn schema_with<F>(f: F) -> SchemaType where F: FnOnce(SchemaBuilderType) -> SchemaBuilderType, { f(schema_builder()).finish() }
また、rust-scriptだと補完が効かなくて作りにくかったのでscripts crateにまとめた。
dataloaderの実装方法自体はドキュメントにコードで記載があり、それをなぞった。以下が私にとってのポイントだった。
Id<User>
をキーにユーザー一覧をとりたいときとボード一覧をとりたいときとかUserIdForBoard
のようなstructにラップして利用すれば良さそう?もしくはdataloaderを分ける?
keys: &[K]
でもらっても変換するのでVecにしている。
HashMap<K, V>
であるので、この形に変換が必要
Data
取得async-graphqlは context
から参照できるデータを持つ。このデータは格納時に Any
にして、取り出すときにダウンキャストしている。
let modules: &Modules = context.data()?; // or let modules = context.data::<&Modules>()?;
そのままでも使えるが、型注釈が必須な点がやや面倒である。
個人的には前者のほうが読みやすいが、続けてメソッドを呼び出したい場合などでは後者の書き方になり、このとき型注釈を忘れるとエラーになってしまったり、 ::<>
のように装飾が多かったりとあまり好きではない。
以下のようにcontextを拡張するメソッドをつくって、型注釈を不要にした。メソッド名から何が返るのかもわかりやすく、実装コストに対して効果的な印象。
// ContextにDataLoader, Modulesを取得するメソッドを作成する // dataはAnyで型消去しているけど、このプロジェクト内ではSchemaを作っている箇所で必ず設定しているはずなので気にしないことにする pub trait ContextExt { fn data_loader(&self) -> Result<&DataLoader<Modules>, GqlError>; fn modules(&self) -> Result<&Modules, GqlError> { Ok(self.data_loader()?.loader()) } } impl<'ctx> ContextExt for Context<'ctx> { fn data_loader(&self) -> Result<&DataLoader<Modules>, GqlError> { self.data() } }
sqlxはマクロで「コンパイル時にSQL結果の型をチェック」できる。
これの利用に向けてはちょっとコツがいる。
query!()
マクロの場合)コンパイル時に型チェック=コンパイルしないと型が決まらない
感想としては、「すごいが、コンパイル時チェックなしの関数でもいいかな……」という感じだった。理由は以下。
where
句のパターンを書き換えたいときにつらいquery_file!()
マクロがあれば完全に同じクエリならいけるsqlxの(postgres)クエリを実行するとき、 PgPool
か Transaction<'a, Postgres>
が必要になる。それぞれの使い方は以下の通り。
pool: &PgPool
の場合
.execute(&pool)
とするmut transaction: Transaction<'a, Postgres>
の場合
.execute(&mut transaction)
とするtransaction: &mut Transaction<'a, Postgres>
の場合
.execute(&mut **transaction)
とする特にC言語の経験からすると3番目は謎が深いが、とりあえず覚えることにする。
derefして &Connection
に、更にderefして '&mut
を取っている感じで認識している。
sqlxがCLIを提供していて、それを通してmigrateした。
migrateファイルの作成も適用もCLIで問題なく行えた。
RustのDIライブラリ。async-graphqlのcontextと組み合わせて、以下のようにしてクエリを取得したい。
let user_query: Box<dyn UsersQuery> = context.modules()?.query().provide()?;
これは工夫しなくても実現できる……が、エラー型がちょっと扱いにくい。最終的には async_graphql::Error
型にしたいが、 shaku::HasProvider::provide()
が返すエラーは Box<dyn std::error::Error>
であり async_graphql::Error
型に変換できない。
そこで、この変換だけをラップするためのメソッドをtraitで拡張して作成した。
// shakuのErrorをasync-graphqlにあわせる pub trait HasProviderGql<I: ?Sized>: HasProvider<I> { fn provide_gql_result(&self) -> Result<Box<I>, GqlError>; } impl<T, I: ?Sized> HasProviderGql<I> for T where T: HasProvider<I> + ?Sized, { fn provide_gql_result(&self) -> Result<Box<I>, GqlError> { self.provide().map_err(|e| { GqlError::new(format!( "providing <{}> failed: {}", type_name::<I>(), e )) }) } }
これを使うと、GraphQL クエリメソッド内でも ?
でエラー型を変換できる。
使い込んでいないので3.5での結果。
ありきたりなkanbanアプリのドメインモデル図だったので、ChatGPTに書かせた。
Kanbanアプリケーションをドメイン駆動設計で作成します。ドメインモデル図をmermaid形式で示してください。
指示としては以上で、それ以降は「集約を小さくしてください」「操作を追加してください」などを追加指示した。
結果として取得できたコードは以下で、いくつか間違っている箇所がある。手動で直せばいい範囲なので、ChatGPTに再修正させず手で直した。
classDiagram class User { <<Entity>> +userId: int +name: string +email: string +boards: Board[] } class Board { <<Aggregate>> +boardId: int +title: string +owner: User +members: User[] +columns: Column[] +addColumn(title: string): Column +removeColumn(columnId: int): void } class Column { <<Aggregate>> +columnId: int +title: string +tasks: Task[] %% --------間違った結果の箇所: addColumnではなくaddTaskっぽい。-------- +addColumn(taskTitle: string, taskDescription: string): Task +removeTask(taskId: int): void } class Task { <<Entity>> +taskId: int +title: string +description: string +status: string } User --> Board : owns User --> Board : member of Board --> Column : has Column --> Task : contains
同じ理屈で、オニオンアーキテクチャのレイヤー構造を図にしてもらった。
mermaidの図の書き方を思い出すより早くできた。
docker-compose.yml
を書かせるRDBを構築したくなったタイミングでChatGPTに docker-compose.yml
も書かせてみた。
今回は典型的な(後で使うつもりのlocalstackと)Postgresを立てるだけだったので、ほとんどそのまま出力できた。
注意点は以下。
60000
から振るように追加指示したRDBのテーブル定義を作るのが面倒だったので、吐き出させた。
何を入力にしたかというと、GraphQL Schemaを入力にした。
以下graphqlのデータをPosgresで構築します。テーブル定義のDDLを示してください。
追加注文したのは以下の通り。
Id
は VARCHAR
型にしてくださいinfrastructure層の実装が終わって、RDBと接続して確認したかったが、データがない。
仮実装の段階でRustコードでデータをメモリ上に作る実装があったので、これを入力にINSERT文を吐かせてみたら、思ったより精度良く吐かせることができた。
以下Rustコードで作成するのと同等のデータをPostgreSQL DBに用意するINSERT文を示してください。 [Rustコード...] なお、テーブル定義は以下のとおりです。 [テーブル定義...]
特に、配列参照を使ってIDがおなじになるようにしていた箇所が全体的に正しく解決されていたので驚いた。
レイヤーの関係で変換が必要な presentation_graphql::scalar::Id<T>
と domain_util::Identifier<T>
とか、クエリの返却値とgraphql responseとか、細かい変換をいくつも書いた。
frunkという関数型プログラミング系のライブラリがあって、この中のGenericという機能がこの辺りの変換をサポートしてくれていそう。活用したら楽になりそう。
互いのテストが影響しないようにするために、インメモリDBを使いたいが、sqlxでそのあたりうまくできるのかを調査する必要がある。
まだアプリケーション内での認証処理とかについて調べていないので確認する必要がある。
しばらく前から、Scalaを書く機会が訪れた。NeoVimのLanguage Clientプラグインとしてはcocを使っているので、Scalaでもcoc-metals
を入れて使うことにした。
https://github.com/scalameta/coc-metals
2022年3月に coc-metals
の開発終了が宣言された 。
なんとかしなきゃなと思いつつも、ひとまず現在のプロジェクトでは問題なく動いていたので、そのまま使い続けていた。
2022年10月、現在のプロジェクトのScalaバージョンアップに伴って、ついにいくつかの機能が動かなくなってしまった。具体的には補完と定義ジャンプあたりができなくなった。
補完と定義ジャンプくらいは動かないと開発しづらいので、これらをできるように色々試した。
coc-metals
のREADMEでは「 nvim-metals
でNeoVim組み込みのLanguage Clientを変わりに使おう」と書いてある。しかし、他のcocプラグインなどもあったり一度慣れた操作を全て捨てなくてはいけなかったりで、これを理由に乗り換えるのは現実的ではないなと思った。
よって、「coc上でmetalsを使う方法」を模索することにした。
とりあえずcocとmetalsの公式を見てみた。coc向けのmetalsの導入方法はどちらも手動でのインストールを推奨していた。
しかし、手動でのインストールは腰が引けた。私の開発環境は複数あって、そのうち特定の環境でしかScalaを利用しないし、ビルドツールの導入から必要なようだったので単純にめんどくさいなと思った。これまで coc-metals
にmetalsの管理を任せていたので、余計に手間だと感じたのかもしれない。
いったんmetalsをそのまま使える方向を模索しようと思った。
coc-metals
が参照するmetalsのバージョンアップ正直今までREADMEに従ってただ導入していただけだったので、なぜ動かなくなったかの調査から始めた。metals起動時にバージョンに関する警告が出ており、ここに 0.11.2
を利用していると書かれていた。調査時点でリリースされているのは 0.11.9
まであったが、 coc-metals
の最新版が参照しているのは 0.11.2
であるようだ。そこでこのバージョンを上げれば直るのではと考えた。
coc-metals
のREDMEに設定項目について記載されており、 metals.serverVersion
に 0.11.9
などを入れれば良さそうだ。
設定してみたところ、metalsのダウンロードができなくなり、起動に失敗するようになってしまった。
coc-metals
は metals-languageclient
の 0.4.2
を利用している。metalsのダウンロードも metals-languageclient
の機能だ。そして metals-languageclient@0.4.2
では、metalsをダウンロードするScalaのバージョンは 2.12
に固定されていた。
metalsのリリースノートを見ると、metalsの 0.11.3
からビルドするScalaのバージョンが 2.13
になったっぽい。なので org.scalameta:metals_2.12:0.11.9
をダウンロード出来ずに失敗してしまうらしい。
coc-metals
の依存する metals-languageclient
のバージョンアップそれならということで、 coc-metals
の依存する metals-languageclient
のバージョンアップをやってみることにした。いわゆるモンキーパッチだ。
coc-metals
はこれまで :CocInstall
でインストールしていた。これはnpmから取得しているので、これにパッチを当てるのは無理だ。cocのドキュメントを見ると、gitでのインストール方法が書いてあった(ただし、依存ライブラリをビルドする関係でディスクスペースを多く使うので非推奨らしい)。
そこで、以下のようにとりあえず coc-metals
をcloneしてNeoVimで読み込めるようにした( yarn
は適当に準備)。
# 適当なディレクトリ cd /path/to/directory git clone https://github.com/scalameta/coc-metals.git cd coc-metals yarn install # NeoVimの設定ファイルにて :set runtimepath^=/path/to/directory/coc-metals # 読み込めると以下コマンドの結果に `coc-metals` が増える :echo map(CocAction('extensionStats'), {k, v -> v['id']})
インストールは出来たので、 yarn upgrade --latest metals-languageclient
にて更新した。その結果、 yarn build
が通らなくなってしまった。インターフェースが色々変わったようだ……
metals-languageclient
の最新は 0.5.17
だったので、バージョン飛ばし過ぎかなと思って 0.5.1
(0.5系の最初)まで戻したのだけど、ビルドできない状況は変わらなかった。これまでコントリビュートしてこれなかった身としてはエラーをすぐに解決することは無理そうだった。ついでに、これに関するマイグレートイシューが立っていて、すでに試されていることであると知った。
ここまできて coc-metals
を使い続けるのは無理だなと判断した。そのため、metalsを手動で入れて利用する方向にかじを切った。metalsの手動インストール方法については、ドキュメントに載っていたため、これを参考にビルドしていった。
私の環境にはJavaは導入済みだったので、あまりそれについては考えずに、未インストールのCoursierからインストールした。
上記Coursierのドキュメントに従って、
brew install coursier/formulas/coursier
とした。すでに sbt
等は sbtenv
などで用意していたため、 cs setup
は行わなかった。
前掲のmetalsのドキュメントに従って、以下のようにした。オプションは吟味していない。
cs bootstrap \ --java-opt -Xss4m \ --java-opt -Xms100m \ --java-opt -Dmetals.client=coc.nvim \ org.scalameta:metals_2.13:0.11.9 \ -r bintray:scalacenter/releases \ -r sonatype:snapshots \ -o ~/bin/metals-vim -f
変更点は以下の通り。
coursier
コマンドを適宜インストールして使うのではなく、前節で brew install
したものを使う
cs
で起動する/usr/local/bin
ではなく、 ~/bin
にするmetals.client
はnvim-lsp以外にも設定可能な値があるようだった。以下を参考に coc.nvim
にした。設定値が変わる模様。
github.com
前節で ~/bin
へ配置したが、普段自分でビルドしないので、ここにはパスが通っていなかった。私はzshなので、 ~/.zprofile
に以下を追記した。
[ -d ~/bin ] && PATH="$HOME/bin:$PATH"
coc-nvim
のwikiに設定があるので、そのまま coc-settings.json
へコピペした。
"languageserver": { "metals": { "command": "metals-vim", "rootPatterns": ["build.sbt"], "filetypes": ["scala", "sbt"] } }
ここまでの設定でNeoVimでScalaファイルを開き動作確認をした。
coc-metals
にあった metals.build-import
などのコマンド感触としては、動いてはいるけど十分ではない、という感じだ。とはいえ、スタートラインには立ててはいるようだ(ちなみに、この段階だと「この先試行錯誤してもだめ」な可能性はあって、戦々恐々としながら進めていった……)。
動いている気はするものの裏付けがなかったので、これを確認した。coc-nvim
のwikiにDebug language serverという節があった。これを参考に試していった。
まず、 :CocList services
を見てみたところ、 languageserver.metals
は [running]
となっていた。問題なく起動は出来ているようだ。
次に、metalsのログを見てみた。 :CocCommand workspace.showOutput
から、 languageserver.metals
を選ぶ……が、ほとんどログが出ていなかった。タイミングによっては(ログがないからか)選ぶことすらできない。
ドキュメントを見直すと、 "trace.server": "verbose"
を追加するとLSP通信の内容が全部出るようだったので、これを追加してやり直したところ、詳細なログを得ることができた。
ファイル内のdocumentHoverは思ったとおりうまくいっているらしい。
(ファイル内のdocumentHover) [Trace - 1:50:37] Sending request 'textDocument/hover - (4)'. Params: { "textDocument": { "uri": "(省略)" }, "position": { "line": 10, "character": 25 } } [Trace - 1:50:38] Received response 'textDocument/hover - (4)' in 485ms. Result: { "contents": { "kind": "markdown", "value": "(省略)" } }
ファイルをまたぐdocumentHoverは思ったとおり何も返ってきていなかった。
(ファイルをまたぐdocumentHover) [Trace - 1:52:43] Sending request 'textDocument/hover - (5)'. Params: { "textDocument": { "uri": "(省略)" }, "position": { "line": 14, "character": 23 } } [Trace - 1:52:43] Received response 'textDocument/hover - (5)' in 14ms. No result returned.
ところで、ここまでで出たログを眺めていて、 coc-metals
を使っていたときには出ていたプロジェクトのビルドのログがないことに気がついた。 coc-metals
はログをechoする仕組みだったのに対して、cocから直接languageserverを使った場合はechoされない。このことは知っていたので、ビルドのログが無いことに疑問を持たなかったのだ。
build-import
をかけるビルドするために何を行えばいいかはおよそ見当がついていて、metalsの調子悪いときに :CocCommand
の中から実行していた build-import
をすれば良さそうだろうと考えていた。しかし、今までは coc-metals
が :CocCommand
に追加してくれていたので、 coc-metals
を使わなくなった今どうやって実行すれば良いのかが分からなかった。
とりあえず coc-metals
のコードからなんとなく command
などで検索してみると、起動時にコマンド登録しているっぽいコード が見つかった。このコマンド群の定義は metals-languageclient
の ServerCommands
に定義されていた。
実際に叩いているコマンドの文字列はわかった。次はコマンドの実行方法だ。先の metals-languageclient
のところにserver commandsと書いてある。関係がありそうなものをcocのhelpから探していると、 :call CocAction("runCommand", "name")
とすることでコマンドが実行できるらしい。
試しに :call CocAction("runCommand", "build-import")
を実行したところ、30秒ぐらい制御を取られ、何かが実行された。その後、documentHoverの情報を改めて確認すると以下の変化があった。
フイル内のdocumentHoverは元々出ていたが、 range
が追加された。
(ファイル内のdocumentHover) [Trace - 2:07:21] Sending request 'textDocument/hover - (11)'. Params: { "textDocument": { "uri": "(省略)" }, "position": { "line": 10, "character": 20 } } [Trace - 2:07:21] Received response 'textDocument/hover - (11)' in 99ms. Result: { "contents": { "kind": "markdown", "value": "(省略)" }, "range": { "start": { "line": 10, "character": 20 }, "end": { "line": 10, "character": 29 } } }
また、ファイルをまたぐdocumentHoverの情報も以下のように出るようになった。
(ファイルをまたぐdocumentHover) [Trace - 2:09:35] Sending request 'textDocument/hover - (12)'. Params: { "textDocument": { "uri": "(省略)" }, "position": { "line": 14, "character": 21 } } [Trace - 2:09:35] Received response 'textDocument/hover - (12)' in 22ms. Result: { "contents": { "kind": "markdown", "value": "(省略)" }, "range": { "start": { "line": 14, "character": 21 }, "end": { "line": 14, "character": 24 } } }
これで一旦の目的を果たした。しかし、使いにくい点があるのでもう少し改善したいと思う。
直前のコマンド( :call CocAction(...)
)は、完了するまで制御を返さない。 build-import
はプロジェクトサイズによっては結構時間がかかるので、長い時間NeoVimが操作を受け付けなくなる。これは非常に使いにくい。
この解は簡単で、 :call CocActionAsync(...)
という非同期版を使えば良い。この2つの関数は、アクションの実行結果の扱いの違いがある。
:echo CocAction("commands")
などを叩いてみると結果が表示される)call CocAction("commands", {err, result -> ...})
みたくするらしいが試してない……ちなみに非同期なのでechoしても当然何も表示されない) :call CocActionAsync("runCommand", "build-import")
を打てばいいことはわかったが、毎回打つにはつらすぎる。
NeoVimなので、必要なカスタマイズは自分ですればいい。キーバインドやコマンドを定義するのが普通だが、今まで :CocCommand
から呼び出していたので、それと同じ操作感にしたい。
cocのhelpを見ると、 coc#add_command()
という関数があった。これを使うと CocCommand
で呼び出せるコマンドを追加できるようだ。これを使って coc#add_command("metals.build-import", ":call CocActionAsync('runCommand', 'metals.build-import')")
とすることで、 :CocCommand
の一覧に表示されるようになった。
うまくいくことがわかったので、サーバーコマンドとして一括で登録する define_metals_commands
というlua関数を書いた。
local add_coc_command = function(name, command, description) vim.fn["coc#add_command"](name, command, description) end local define_metals_commands = function() local commands = { "ammonite-start", "ammonite-stop", "analyze-stacktrace", "bsp-switch", "build-connect", "build-disconnect", "build-import", "build-restart", "compile-cancel", "compile-cascade", "compile-clean", "copy-worksheet-output", "debug-adapter-start", "doctor-run", "file-decode", "list-build-targets", "generate-bsp-config", "goto", "goto-position", "goto-super-method", "new-scala-file", "new-java-file", "new-scala-project", "reset-choice", "scala-cli-start", "scala-cli-stop", "sources-scan", "super-method-hierarchy", } for _, v in pairs(commands) do local name = "metals." .. v local action = ":call CocActionAsync('runCommand', '" .. v .. "')" add_coc_command(name, action) end end
これで一通りのコマンドを追加することが可能だ。私は全部使っているわけではないが、一旦全部登録したままにしている。コマンド名に metals.
とつけているので、他のものと競合することはないはずだ。
ここまでで一応使える状況にはなったが、別環境のことを考慮していなかった。 metals-vim
がPATHにない状態でScalaファイルを開くとcocがエラーを出す。
前述のように coc-settings.json
に書いていると、条件分岐を行う方法がない。ドキュメントを眺めると coc#config()
という関数でも coc-settings.json
と同等のことが行えるらしい。これなら、コードからの呼び出しなので分岐は自由だ。
coc-settings.json
をやめてluaで書くcoc#config()
を使っていこうと思ったのだが、 coc-settings.json
との併用は非推奨らしい。
仕方がないので、全てをluaで置き換えた。文法は大きく違わないので、以下に注意してluaのtableに置き換え、登録していった。
ちなみにこのあたりの書き方はいつもluaのマニュアルの(非公式)日本語訳を見ている。
ただし、1箇所読んだだけだとわかりにくかったり、そもそもどこに書いてあるのか目次からわからなかったりする。
たとえば、前述のテーブルの記法の件は「3.4.9 - テーブルコンストラクタ」にある。変数や型の節ではない。3章までよめば文法はわかるので、NeoVimでluaを使うならここまで全部読んでしまってもいいかもしれない。文法以外の関数(たとえば for
でテーブルを走査するときによく使う pairs()
)などは、ググった方が早い。ググっても載ってない細かい仕様が必要な場合だけドキュメントに戻ってくるので十分だ。
NeoVim側のluaのAPIについてはこのドキュメントを見ている。あとは coc-sumneko-lua
の補完に頼っている。更に情報がほしいときだけ、 :h nvim_echo()
などで api.txt
を読んでいる。関数の存在を和訳ドキュメントで見て、細かいオプションはhelpを見るのが丁度いいかなと思う。
luaに置き換えると、例えば先に示したmetalsの設定の場合は以下のようになる。
local languageserver_config = {} languageserver_config.metals = { ["command"] = "metals-vim", ["rootPatterns"] = { "build.sbt" }, ["filetypes"] = { "scala", "sbt" }, } -- 実際に呼んでいるのは coc#config("languageserver", languageserver_config) My.PutCocConfig("languageserver", languageserver_config)
これなら分岐でもなんでも仕込み放題だ。ついでにコメントも書ける。
前節のような感じで置き換えが出来たので、 metals-vim
の実行ファイルがある時だけ上記の部分のコードを追加したい。
まずは実行可能ファイルがあるかのチェックをする。これはNeoVimの関数を利用するのが楽っぽいので、以下関数を定義した。ちなみに My
というのは、変数や自作関数を作ったときに置いておくグローバル変数として自分で定義したものだ。
local function has_executable(name) return vim.fn.executable(name) == 1 end My.has_executable = has_executable
これを使って、
if My.has_executable("metals-vim") then -- 前述のコード end
とした。これでいい感じだ。
ここまででやっと使える域に達したと感じることができた。
とりあえず動くようになったが、出来ていないこともありそのうちなんとかしたい気持ちはあるので書き残しておく。
CocCommand
への登録を遅らせる上記の方法だと、metalsがないときには CocCommand
へ登録されないが、あるときにはcoc起動時に登録される。今まで coc-metals
ではサービス起動時(≒Scalaファイルを開いたとき)に登録されていたので、そちらに近づけたいと思った。
調べた限りでは、サービスステータスの変更に伴ってCallbackが呼ばれるような仕組みが見当たらず、断念した。
以下が似た投稿で、 user CocNvimInit
イベントを使っていたが、このイベントはcocの起動時に呼ばれるため、coc起動後にサービスが立ち上がった場合(私は普段そうなることが多い)に対応できなかった。
まぁ、コマンドが一覧に出るだけで他のコマンドを叩きたいときはたいてい絞り込んで使うため、あまり邪魔にはなっていないのでよしとした。
build-import
コマンドを実行するこれも上記と同様の理由でトリガーできないため断念した。一旦手動で叩く手間を軽減したので、しばらくは様子見することにした。
頑張れば書ける……が、インストールする頻度が低いし、設定ファイルにインストールスクリプトが載っていればとりあえずはOKということにした。
以下が面倒だった。
coursier
なのか cs
なのか、そもそもあるのかどうかmetals(に限らないけどLanguageServer)は起動にちょっと時間がかかる。私の環境では1〜2分くらい。
その間にcocの機能でdocumentHoverすると Hover Not Found
になってしまう。
終わったかどうかのチェックは CocAction workspace.showOutput
で languageserver.metals
を目視するか、「そろそろ大丈夫だろう」という勘でやっている。目視の方は、別のウィンドウが開かれるので鬱陶しいし、勘は間違っていることが度々ある。
lightlineを使っているので、ここに表示したいが、このあたりを調べきってない。今は基本勘で、ダメそうだったら showOutput
出しながら目視するくらいにしている。
build-import
しても別のファイルを開くとdocumentHoverできなかったりする要するに完璧に動くわけじゃないのだけれど、症状を言語化できていない。今動く範囲でコーディングが可能なので放置している。
このあたりからどうでもいい話になってくる。
CocCommand
に登録するコマンドの一覧を、何かと連携して作っているわけではなく、参考リポジトリからコピペして作っている。これだとコマンドが増えたときに気づけない。
ただ、そんなに新しいコマンドが必要なこともなさそうだし、追加するにしても配列に1行足す程度なので手間じゃない。そういうわけで放置することにした。
本当に軽微な話だけど、 coc#add_command("metals.build-import", ...)
としているのに、コマンド名は vim.metals.build-import
となる。
見た目が気になるだけだし直し方が分からないので放置している。
全部完璧にできたわけじゃないが、コードを読んだり書いたりはできるようになった。また気が向いたら調整すると思うけど、一段落できて安心した。
一番精神的に大きな話。
coc-metals
の開発終了は少し前から知っていたけれど、その時はまだ動いていたのもあって、メンテナには手を挙げなかった(挙げられなかった)。今回、実際動かなくなってから色々調べたりパッチを当てようとしたりして、最終的にメンテナに手を挙げることができなかった。
metals-languageclient
についての知識不足でコードの修正方法が分からなかったのもある。英語のReading以外がとても苦手というのもある。だが自分の中で一番大きかったのは「Scalaを専門で書いているわけではないので時間を割くのが難しい」と思ってしまった点だ。
そう思ったときに「やっぱり自分もフリーライディングしてるし望んでるんだな」と気づいた。誰かがメンテしてくれる安定した無料のソフトウェアを使いたいだけだった。
これは結構へこんだし、しばらく向き合い方に悩んだが、今私にできる貢献としてはこういうブログを書いて同様に困る人を減らすことくらいだろうということに落ち着いた(あとはシンプルにお金を払うとかだろうか……これも追って検討したい)。英語がもうちょっと書けたら、cocにサービスステータス反応でアクションを仕込む方法を聞くか要望上げるかできるんだけど……
どこかで「LSPはプロトコルが整理されているので、クライアントではサーバーの参照方法だけ設定に書けば使えるしプラグイン入れるのは無駄!」みたいな論を見た気がするのだけれど(掘り起こせなかった……)、少なくとも今回はこうはならなかった。
今回で言えば、以下のことをしなければいけなかった。ただしmetals以外でも同様かどうかは調べてない。
今まではほとんどの設定をvimscriptで書いていたが、今回初めて大掛かりな設定をluaで書いたところ、なんとなく書きやすいなと感じた。以下のようなあたりが理由だと思う。
a:
としないといけないとかaugroup END
とか\
が不要let
しなくても変数が書き換えられるcall
がなくても関数を呼び出せるまぁ、そもそもvimscriptはvimコマンドの集合だし、そりゃプログラミング言語で書いたほうが書きやすくて当然である。
気持ちとしては全部luaにしてもいいなぁと思っているが、ちょっとずつ移行していけばいいかなと思った。
私のneovim config はこちらだ。
開発するためにエディタを整えているのか、エディタを整えるために開発しているのか分からなくならない程度にこれからも頑張っていきたい。
この記事は Rust Advent Calendar 2021 19日目の記事です。
こちらは、やってみたけどうまく行かなかった記事です。 以下について情報提供をお待ちしています。
bootstrap
に引数は渡せない?AWS Lambdaがコンテナイメージでの実行をサポートしてしばらくが経ちます。 RustでLambdaのコンテナイメージを作る場合、1つのイメージにつき1つのバイナリが必要そうでした。
Python等でLambda コンテナイメージを作る場合は、1つのイメージに複数のhandlerを含め、CMDで実行するものを変更できました。これと同様のことがRustでもできるのではないでしょうか?
1つのイメージに複数のhandlerを含めたときに気になるのが、バイナリサイズや実行時間への影響です。今回はこれらをローカル実行にて確認してみたいと思います(思っていました)。
以下3種類のLambda用バイナリを作ります。
simple-hello
: {"message": "Hello, World!"}
を返すだけのシンプルなLambdasimple-regex
: 入力の date
が、文字列で YYYY-MM-DD
の形になっているか確認するLambdacli-mixed
: 上記2つのLambda handlerをCLI引数で実行し分けるLambdaこれらについて、以下を(ローカルで)確認します。
GitHubに配置しました。
以下のようにそれぞれビルドします。
cargo build --release --target x86_64-unknown-linux-musl docker build -t simple-hello -f Dockerfile-simple-hello . docker build -t simple-regex -f Dockerfile-simple-regex . docker build -t cli-mixed -f Dockerfile-cli-mixed .
ls
で普通に確認します。
s -lh target/x86_64-unknown-linux-musl/release/ 合計 21M drwxr-xr-x 15 vraisamis vraisamis 4.0K 12月 18 21:14 build -rwxr-xr-x 2 vraisamis vraisamis 7.7M 12月 18 21:14 cli-mixed -rw-r--r-- 1 vraisamis vraisamis 162 12月 18 20:50 cli-mixed.d drwxr-xr-x 2 vraisamis vraisamis 20K 12月 18 21:14 deps drwxr-xr-x 2 vraisamis vraisamis 4.0K 12月 18 20:49 examples drwxr-xr-x 2 vraisamis vraisamis 4.0K 12月 18 20:49 incremental -rwxr-xr-x 2 vraisamis vraisamis 5.6M 12月 18 20:50 simple-hello -rw-r--r-- 1 vraisamis vraisamis 168 12月 18 20:50 simple-hello.d -rwxr-xr-x 2 vraisamis vraisamis 7.1M 12月 18 20:53 simple-regex -rw-r--r-- 1 vraisamis vraisamis 168 12月 18 20:50 simple-regex.d
バイナリサイズについてですが、以下記事で言及されているサイズとも大きく違いません。おかしいところはないと言えるでしょう。
また、cli-mixed
での増分は0.6MBほどで、CLIパースライブラリ( clap
3)を入れることによる影響は小さいものと考えて良さそうです。
docker images REPOSITORY TAG IMAGE ID CREATED SIZE cli-mixed latest a1edde0b912e 25 hours ago 312MB simple-regex latest 140072c9464d 26 hours ago 311MB simple-hello latest 5e7d492f9742 27 hours ago 310MB public.ecr.aws/lambda/provided al2 606c70acc7a0 36 hours ago 304MB
上記の通り、ベースのイメージは300MBほどあります。最小のLambdaであれば、イメージサイズに比べて十分小さく、デプロイ速度等への影響は出にくいものと考えられます。
いずれも以下のようにしてローカル実行します。
docker run --rm -p 9000:8080 simple-hello # 別terminalで curl -XPOST "http://localhost:9000/2015-03-31/functions/function/invocations" -d '{"date": "2014-01-01"}' # docker runしたほうのterminalに以下が表示される START RequestId: 4965b0b0-8129-427d-9551-292b0391e4dd Version: $LATEST END RequestId: 4965b0b0-8129-427d-9551-292b0391e4dd REPORT RequestId: 4965b0b0-8129-427d-9551-292b0391e4dd Init Duration: 0.35 ms Duration: 5.25 ms Billed Duration: 6 ms Memory Size: 3008 MB Max Memory Used: 3008 MB
cli-mixed
は以下の通り実行できませんでした。
docker run --rm -p 9000:8080 cli-mixed hello time="2021-12-19T14:25:39.891" level=info msg="exec '/var/runtime/bootstrap' (cwd=/var/task, handler=)" time="2021-12-19T14:25:43.398" level=info msg="extensionsDisabledByLayer(/opt/disable-extensions-jwigqn8j) -> stat /opt/disable-extensions-jwigqn8j: no such file or directory" time="2021-12-19T14:25:43.398" level=warning msg="Cannot list external agents" error="open /opt/extensions: no such file or directory" START RequestId: 8410dbfa-8b8a-43a1-80a4-91f3b48fee35 Version: $LATEST cli-mixed USAGE: bootstrap <SUBCOMMAND> OPTIONS: -h, --help Print help information SUBCOMMANDS: hello help Print this message or the help of the given subcommand(s) reg time="2021-12-19T14:25:43.402" level=warning msg="First fatal error stored in appctx: Runtime.ExitError" time="2021-12-19T14:25:43.402" level=warning msg="Process 16(bootstrap) exited: Runtime exited with error: exit status 2" time="2021-12-19T14:25:43.402" level=error msg="Init failed" InvokeID= error="Runtime exited with error: exit status 2" time="2021-12-19T14:25:43.402" level=warning msg="Reset initiated: ReserveFail" time="2021-12-19T14:25:43.402" level=warning msg="Cannot list external agents" error="open /opt/extensions: no such file or directory" cli-mixed USAGE: bootstrap <SUBCOMMAND> OPTIONS: -h, --help Print help information SUBCOMMANDS: hello help Print this message or the help of the given subcommand(s) reg END RequestId: 5739ea46-a317-4581-b9c8-dc073f0e7b1f REPORT RequestId: 5739ea46-a317-4581-b9c8-dc073f0e7b1f Init Duration: 0.23 ms Duration: 8.27 ms Billed Duration: 9 ms Memory Size: 3008 MB Max Memory Used: 3008 MB
いずれも10回実行した結果です。 (最小)~(平均)±(分散)~(最大) で表記します。
プログラム | Init Duration [ms] | Duration [ms] | Memory Size [MB] | Max Memory Used [MB] |
---|---|---|---|---|
simple-hello | 0.18~0.33±0.10~0.49 | 3.76~4.52±0.47~5.25 | 3008 | 3008 |
simple-regex | 0.19~0.31±0.06~0.40 | 4.60~5.17±0.51~6.15 | 3008 | 3008 |
simple-regex
が正規表現パターンをコンパイルするので、その分だけ遅くなっているようですcli-mixed
について比較できていないのが残念ですいずれも10回実行した結果です。
プログラム | Duration [ms] | Memory Size [MB] | Max Memory Used [MB] |
---|---|---|---|
simple-hello | 1.24~1.35±0.10~1.54 | 3008 | 3008 |
simple-regex | 1.03~1.28±0.17~1.67 | 3008 | 3008 |
simple-regex
が2回目以降パターンがコンパイル済みであるため、もともとあった差がよりなくなっています(平均で見るとちょっと速い)cli-mixed
について調査完了できませんでした。 実行方法について情報をお待ちしています