RareJob Tech Blog

レアジョブのエンジニア・デザイナーによる技術ブログです

4種のPromiseメソッドの違いについて

長引くコロナ禍の中、お家時間をより充実したものにするために社内開発メンバーの皆さんにオススメ家電を聞いたり実際の使用感の伺ったりできるslackチャンネルを作ったことが上半期の自身の業務成果かなと思っているフロントエンドエンジニアの田原です。 \(^o^)/(お仕事もしてます

生活スタイルが皆さん異なるのでオススメ家電や感想等が違い色んな生の意見をもらえるので新しい発見もあり、まだまだリモートワークが続く中で比較的どんな人でも (家電は誰でもつかうと思うので)参加しやすい話題な気もしているのでコミュニケーションの一助にもなっていたら良いなと思ったりしております。

チームリーダーオススメのNature Remoはこの夏に購入して(というかこれ書いてる時にポチった)色んな家電をより使いやすくしたいなと画策しており、先行予約で手に入れたAladdin Connectorはまだ開封すらしていませんが、そろそろNintendo Switchと接続して夏のお家時間をより充実させたものにしたいなと思っております。

お話かわりまして、ES2021で追加予定の新しいメソッドのanyも含めてPromiseの静的メソッドについて違いや使い所等について今回は触れていきたいなと思います。

※尚、JavaScriptにおける非同期処理についてやPromiseの概要、await/async等については世の中に良い記事が溢れているので説明は割愛させて頂きます。

目次

4つの静的メソッド

1つの非同期処理についてはawait/asyncを使うことが多くなってきたかと思いますが、複数処理を扱う際のメソッドとして実験的なanyを含めて4つの静的メソッドが存在しており、これらを適所で使うことで非同期処理のハンドリングをよりパフォーマンスの良いものにすることが可能になります。

Promise.all

非同期処理を並列で同時実行しすべての非同期処理が解決(正常終了)するか、同時実行の内の1つでも拒否(Error等)されるまで待ちます。

【人で例えると】
約束の内容は全部守ろうとしてくれる義理堅い人(おそらく)

e.g.

async function all() {
  const first = new Promise((resolve, reject) => {
    setTimeout(() => resolve('1秒後に処理が完了'),1000)
  })
  const second = new Promise((resolve, reject) => {
    setTimeout(() => resolve('10秒後に処理が完了'),10000)
  })
  const result = await Promise.all([first, second]).catch((e) => cosole.info('error', e))
  if (result) {
    console.info('success', result)
  }
}
all()

【使い所】
複数のエンドポイントに並列的にrequestを投げ、全てのresponseが正常でない場合はerrorハンドリングする場合等

Promise.allSettled

非同期処理を並列で同時実行しすべての非同期処理が解決or拒否されるまで待ちます。

allは1つでも非同期処理がrejectされると途中まで走っていた別の処理の状況は無視して処理を止めてしまいますが、allSettledは各処理がどちらの 処理になろうとも設定された内容を返すまで待ちます。

ただし、rejectを行ってもcatch節に入らず(その為、allSettledの場合、catchは書かなくて良い)に必ず値が返り、 返ってくる値の内容も特殊で以下のようなobjectの形で返却されます。

{status: "fulfilled", value: xxxx } or
{status: "rejected", reason: xxxx } 

【人で例えると】
上手くいっても失敗しても最後まで約束したことはこなそうとしてくれる義理堅い人(きっと)

e.g.

async function allSettled() {
  const first = new Promise((resolve, reject) => {
    setTimeout(() => resolve('1秒後に処理が完了'),1000)
  })
  const second = new Promise((resolve, reject) => {
    setTimeout(() => resolve('10秒後に処理が完了'),10000)
  })
  const result = await Promise.allSettled([first, second])
  console.info('success', result)
}
allSettled()

【使い所】
複数のエンドポイントに並行的にrequestを投げるが、各responseの状態次第で振る舞いを変えたいとき等

Promise.race

非同期処理を並列で同時実行しどれか1つが解決or拒否されるまで待ちます。 拒否時はall同じになりますが、allと違うのは1つでも解決した場合はすぐに処理が終わるところです。

以下の例ではfirstが解決であっても拒否であっても必ず先に処理が走る為(1000msなので10000msより早い)、secondの処理は待たれることはありません。 番外編で後述しておりますが単体よりも組み合わせて使う際に効果を発揮する気がしております。

【人で例えると】
できる約束の内容を最速で対応してくれる義理堅い人(だと思う)

e.g.

async function race() {
  const first = new Promise((resolve, reject) => {
    setTimeout(() => resolve('1秒後に処理が完了'),1000)
  })
  const second = new Promise((resolve, reject) => {
    setTimeout(() => resolve('10秒後に処理が完了'),10000)
  })
  const result = await Promise.race([first, second]).catch((e) => console.info('error', e))
  if(result){
    console.info('success', result)
  }
}
race()

【使い所】
複数のエンドポイントに並行的にrequestを投げ、一番早く処理される内容次第でハンドリングしたい時等

Promise.any

ES2021に実装される実験的メソッドであり、1つでも解決した時点で終了します。 raceと似ていますが、raceは解決であっても拒否であってもであったのに対し、1つが解決しなくても他のものが解決すればその処理が終わるまで待ちます。 その為、全ての処理が拒否された時にのみcatch節に入ります。

【人で例えると】
約束が全部こなせないとわかるまで投げ出さない義理堅い人(多分)

e.g.

async function any() {
  const first = new Promise((resolve, reject) => {
    setTimeout(() => resolve('1秒後に処理が完了'),1000)
  })
  const second = new Promise((resolve, reject) => {
    setTimeout(() => resolve('10秒後に処理が完了'),10000)
  })
  const result = await Promise.any([first, second]).catch((e) => console.info('error', e))
  if(result){
    console.info('success', result)
  }
}
any()

【使い所】
並行的にrequestを投げた結果、全て拒否されるまではハンドリングするのを待ちたい&全部揃ってなくても返されるresponseだけで良い場合等

番外編

ちなみにここまで説明した各メソッドで他のメソッドをラップして実行を待つこともできます。

e.g.

async function allRace() {
  const first = new Promise((resolve, reject) => {
    setTimeout(() => resolve('1秒後に処理が完了'),1000)
  })
  const second = new Promise((resolve, reject) => {
    setTimeout(() => resolve('10秒後に処理が完了'),10000)
  })

  const result = await Promise.all([
    Promise.race([first, second]),
    Promise.race([first, second])
  ]).catch((e) => console.info('error', e))
  if(result){
    console.info('success', result)
  }
}
allRace()
  • この場合all->raceの処理となり:1秒後にfirstが2回raceとして返され、全ての値が返った状態のallとして配列が返ります。
  • その他、all->rase,allSettledの処理等もできるので組み合わせ次第で色んな待ち方ができそうです。

まとめ

各Promiseメソッドは人に例えるとみんな義理堅い人だったことがわかり.....

弊社フロントエンドではNuxtの機能であるserverMiddlewareを使ってBFFを導入しており、BFFサーバーの実装の中でAPIへのrequest処理をいい感じにしたい部分があったので色々と調べた結果、anyメソッドの登場もあったので(anyについては知らなかったので)備忘録も兼ねて書きました。 改めて色々と調べたりしたのでPromise完全に理解した(してない。でも少しは理解が深まった。)気がします。

弊社内のフロントの構成は以下スライドに内容記載しておりますのでご興味あればご一読頂けると嬉しいです。

参考

Figmaを使ってフロー図を書いてみた

お久しぶりです!デザイナーの渡辺です。

最近は自分が周りの人と違って
人間らしからぬ生活を送っているということに気づき
朝散歩をはじめました!
陽の光を浴びるのは大事ですね。(梅雨に入ったので休憩してます)

さて今回は業務中に、フロー図を書く機会がありましたので
せっかくならFigmaで書いてみようということで
その時どのように書いたのかを簡単にお話できればと思いますφ(..)

そもそもなんでFigmaでフロー図?

今回私がフロー図を書く際の前提として

  • 私はそこまで頻繁にフロー図を書くことはない
  • 大々的に共有するものではなく、軽く流れを提案したい程度
  • 使いやすければチーム内などで提案するというのあり

というとこから始まりました!
なので正直見やすいものが作れるならツールは何でも良かったです!

Figmaを選んだ理由としては
「見栄えのよさげなプラグインがありそう」
という理由です笑

色々見て選んだのは「Omnichart」というファイルです。
今回はプラグインではなくファイルを選んでます!

さっそく使ってみる

Communityから「Omnichart」を検索してDuplicateボタンをぽち。

f:id:yui_1027:20210623120946p:plain

ファイルが複製されるので開いてみます。

f:id:yui_1027:20210623121248p:plain
コンポーネント
f:id:yui_1027:20210623121311p:plain
フロー図とサイトマップの例

Omnichartにはコンポーネント集と
それを使って作ったフロー図とサイトマップの例が入っています。

もうこれコピーして中身変えれば終わりじゃん
そんな感じでサクッと作れてしまいました(゚∀゚)!

f:id:yui_1027:20210623120716p:plain
フロー図の一部

Omnichartのよいところ

基本的にカラー、パーツがコンポーネント化されているので
有料化しているチームにこのファイルを置き
assets→Librariesに登録してしまえば
チーム全員が同じカラー同じ見た目のフロー図が簡単に作れるというところですね!

f:id:yui_1027:20210623121631p:plain

コンポーネント化されているのでカラー変更や形状変更は簡単なので
Figmaに慣れていない方でも触りやすいのではないかなと思います!
カラーも決められている分、 「作る人によって色が違う!」ということになりません。

また、フロー図だけではなくサイトマップも作れるので
実際に作ったデザインとサイトマップを連携させるということも可能です。

Omnichartの注意点

Omnichartはファイルなので有料化されたチームでなければ
コンポーネントを共有できないという点と
ファイルの複製時にコンポーネント化されているパーツは一部になっているので
必要なパーツはチーム単位で1つ1つコンポーネント化する必要があるというところです。

コンポーネント化に関しては、1度やってしまえば終わりですので
少しめんどくさいのですが、やってもよいのかなと思います!

ただ、有料化に関しては使う場面によっては
難しい場合もあるので良し悪しは分かれそうです(._.)ムムム

まとめ

私個人としてはOmnichartを使うことでフロー図の見栄え統一、
サイトマップとのデザインの連携などメリットはあるものの
有料化しないと使い勝手は悪いという結論になりました。

ただ、我々デザインチームは有料チームを使用しているので チーム間での作成は問題なく、
業務効率を上げるためのフロー提案に使っていこうと思いました!

有料化チームがすでにある方は
デメリットはそんなにないと思うので一度試して見てはいかがでしょうか?
他にもよいプラグインやファイルがあるぞー!という方は
教えてくださると嬉しいです(`・ω・´)ゞ

Figmaについてはレアジョブアピールや
他のデザイナーも触れているのでそちらもご覧ください。

appeal.rarejob.co.jp

rarejob-tech-dept.hatenablog.com

それではノシ

GatlingとAWS Batchで作る負荷試験用攻撃サーバー

こんにちは。サービス開発チームの越です。

緊急事態宣言下で、在宅で仕事を行う時間が増えてきている中で、個人的に体調管理の目的でプチ断食を実践中です。

プチ断食をして、1日のうちに16時間程度ものを食べず、空腹時間をつくることで、オートファジーが働き、様々なメリットが得られるとのこと。

オートファジーとは人体の古くなった細胞を、内側から新しく生まれ変わらせる仕組みのことを言うらしいです。

さて、新しく生まれ変わらせるといえば、現在レアジョブ英会話のリプレース業務に携わっているところで、負荷試験を実施したときに使用したGatlingについての記事を書きたいと思います。

※ レアジョブ英会話のリプレース業務についての業務内容の説明はこちら

Gatlingとは

GatlingはHTTPリクエストとレスポンスを高速かつ並列に送受信して、Webアプリケーションの性能試験を行うためのツールになります。

Akkaフレームワークをベースに構築されているため、ノンブロッキングな非同期処理を軽量かつ並列に実行することが可能になっています。

Akka の特徴

Akkaのアクターは以下を提供します。

・分散性、並行性、並列性のためのシンプルで高レベルの抽象化。

・非同期でノンブロッキング、かつ、高性能のメッセージ駆動プログラミングモデル。

・とても軽量なイベント駆動処理(1 ギガバイトのヒープメモリにつき数百万のアクター)。

Akkaのアクターモデルは非常によくできており、Akkaの公式ページによると、1台のマシン上で毎秒5000万メッセージを送受信し、わずか1GBのヒープメモリの利用で250万ものアクターの生成を実現しています。

High Performance

Up to 50 million msg/sec on a single machine. Small memory footprint; ~2.5 million actors per GB of heap.

アクターは3つの要素で構成されており、1つのアクターは1つスレッドを持っています。

  1. 他のアクターから何らかのデータを受信するために用意するメッセージキュー
  2. メッセージキューから受け取ったデータを渡す先となるメッセージハンドラ
  3. メッセージキューからデータを取り出して、それをメッセージハンドラに渡す役目のスレッド

アクターモデルにより、Gatlingでは複数の仮想ユーザーによる、ユーザーシナリオの並列実行が実現されています。

マルチスレッドと非同期処理

非同期処理で重要になるマルチスレッドについて、ここでいうマルチスレッドとは複数のスレッドを使うということです。

OSはCPU上で実行するスレッドを適宜入れ替えます。

この時CPU上で完全に同時に実行可能なスレッドの上限はCPUコアの数と同じになるので、このあたりを踏まえてハードウェアの環境を用意する必要があります。

Gatlingの分散テスト環境を考える

負荷試験を実施しようとするときには、テストで使用するハードウェアの制限を考慮する必要があります。

過去にGatlingのgithubリポジトリのissueでも分散環境の議論がされていました。

[commented on 15 Feb 2012] Have a distributed mode for getting over the hardware and OS limits.

...

[commented on 29 Jul 2015] Done in commercial product

「ハードウェアとOSの制限を克服するための分散モードを用意します。」といって議論が始まり、「市販品の方(https://gatling.io/gatling-frontline/)で対応したよ」といってissueはcloseされている状況みたいなので、お金を払って分散テスト環境を用意するということもできます。

https://github.com/gatling/gatling/issues/415

そこで今回はAWS BatchとOSSのGatlingを使用して、分散テスト環境を構築することを考えてみます。

環境

用意する環境としては以下のような構成で実施する想定で考えます。

  • Gatlingによる負荷試験実施後に生成される実行結果のログファイルをアップロードするためのS3バケット
  • Gatlingを動かすAWS Batch
  • 攻撃対象サーバー

実行結果のログファイルがすべてS3にアップロードされたら、ログファイルをローカルにダウンロードして、レポートを生成します。

f:id:t_koshi:20210606022306p:plain

用意するファイル

Dockerfile
start.sh
gatling/
     ├── results/
     ├── conf/
     └── user-files/
          └── simulations/
                    └── gatlingTest/
                              ├── MainSimulation.scala
                              └── SubSimulation.scala

Dockerfile

  • Amazon Linux2
  • Amazon Corretto(無料の OpenJDK)
  • Gatling
  • aws-cli(S3への実施結果ログのアップロード用)
FROM amazonlinux AS base

WORKDIR /opt

ENV GATLING_VERSION 3.6.0

RUN mkdir -p gatling

RUN amazon-linux-extras enable corretto8 && \
    yum install -y java-1.8.0-amazon-corretto \
    wget \
    unzip

# gatlingのインストール
RUN mkdir -p /tmp/downloads && \
  wget -q -O /tmp/downloads/gatling-$GATLING_VERSION.zip \
  https://repo1.maven.org/maven2/io/gatling/highcharts/gatling-charts-highcharts-bundle/$GATLING_VERSION/gatling-charts-highcharts-bundle-$GATLING_VERSION-bundle.zip && \
  mkdir -p /tmp/archive && cd /tmp/archive && \
  unzip /tmp/downloads/gatling-$GATLING_VERSION.zip && \
  mv /tmp/archive/gatling-charts-highcharts-bundle-$GATLING_VERSION/* /opt/gatling/ && \
  rm -rf /tmp/*

# AWS CLIのインストール
RUN curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" && \
    unzip awscliv2.zip && \
    ./aws/install && \
    rm -rf awscliv2.zip aws

WORKDIR  /opt/gatling
COPY start.sh .
RUN chmod 744 start.sh

ENV PATH /opt/gatling/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
ENV GATLING_HOME /opt/gatling

FROM base AS local
VOLUME ["/opt/gatling/conf", "/opt/gatling/results", "/opt/gatling/user-files"]

FROM base AS aws
COPY gatling/conf /opt/gatling/conf
COPY gatling/user-files /opt/gatling/user-files
CMD ["start.sh"]

start.sh

内容

  1. Gatlingの負荷試験を実施する
  2. 実施した結果のログファイルをS3へアップロードする
#!/bin/sh
echo "AWS_BATCH_JOB_ID=$AWS_BATCH_JOB_ID";
aws --version
java -version

if [ $# != 1 ]; then
    echo ERROR: Uses className as the name of the simulation to be run;
    exit 1
fi

echo "gatling.sh -nr -s $1";
gatling.sh -nr -s $1

LOG_FILE_PATH=$(find /opt/gatling/results -type f -name "simulation.log" | head -n 1)
if [ -n "$LOG_FILE_PATH" ]; then
    echo "aws s3 cp";
    aws s3 ls s3://gatling-batch/$(date '+%Y%m%d')/
    aws s3 cp $LOG_FILE_PATH s3://gatling-batch/$(date '+%Y%m%d')/"$AWS_BATCH_JOB_ID.log" --dryrun
    aws s3 cp $LOG_FILE_PATH s3://gatling-batch/$(date '+%Y%m%d')/"$AWS_BATCH_JOB_ID.log"
else
    echo "simulation.log daes not found"
fi

負荷試験のシュミレーションファイルの用意

今回は以下の2つのシュミレーションファイルを用意しました。

  • MainSimulation.scala(0.5RPSのリクエストを60秒行うシナリオ)
  • SubSimulation.scala(0.2RPSのリクエストを60秒行うシナリオ)

※ RPS:Request Per Second

ローカル環境での動作確認

Docker imageのビルド

$ > docker build --target local -t=gatling-batch-local .

Dockerコンテナの作成とシナリオの実行

$ > docker run -it --rm -v "${PWD}/gatling/conf":/opt/gatling/conf \
-v "${PWD}/gatling/user-files":/opt/gatling/user-files \
-v "${PWD}/gatling/results":/opt/gatling/results \
gatling-batch-local /bin/sh -c "gatling.sh -s computerdatabase.BasicSimulation"

AWS Batchの環境用意

AWS ECRへpushするDocker imageのビルド

$ > docker build --target aws -t=gatling-batch .

Docker imageのビルドが完了したら、ECRへpushして、AWS Batchの環境構築を行います。

ECRにpushしたimageを実行するための環境を用意します。

  • ジョブ定義
  • ジョブキュー
  • コンピューティング環境(インスタンスロールにS3へアップロードできる権限を付与しておきます)

並列で実行したいので、ジョブキューにコンピューティング環境を複数台登録します。 f:id:t_koshi:20210606004057p:plain

AWS Batchの実行と結果

以下のようにジョブの送信を行います。

今回はMainSimulationを2つ、SubSimulationを1つ並列で実行させてみます。

$ > aws batch submit-job --job-name gatling-batch-job --job-queue gatling-batch-queue  --job-definition gatling-batch:2 --container-overrides command="/bin/sh","start.sh","gatlingTest.MainSimulation"

$ > aws batch submit-job --job-name gatling-batch-job --job-queue gatling-batch-queue  --job-definition gatling-batch:2 --container-overrides command="/bin/sh","start.sh","gatlingTest.MainSimulation"

$ > aws batch submit-job --job-name gatling-batch-job --job-queue gatling-batch-queue  --job-definition gatling-batch:2 --container-overrides command="/bin/sh","start.sh","gatlingTest.SubSimulation"

並列に実行されてそうですね。 f:id:t_koshi:20210606005227p:plain

実行が完了すると、S3のバケットに結果のログファイルがアップロードされました。 f:id:t_koshi:20210606005044p:plain

レポートの生成のために、実行結果のログをS3からローカルにダウンロードします。

$ > aws s3 cp s3://gatling-batch/$(date '+%Y%m%d')/ gatling/results/aws-batch/ --exclude "*" --include "*.log" --recursive

最後に実行結果のログからレポートを生成します。

$ > docker run -it --rm -v "${PWD}/gatling/conf":/opt/gatling/conf \
-v "${PWD}/gatling/user-files":/opt/gatling/user-files \
-v "${PWD}/gatling/results":/opt/gatling/results \
gatling-batch-local /bin/sh -c "gatling.sh -ro /opt/gatling/results/aws-batch"

f:id:t_koshi:20210606011323p:plain f:id:t_koshi:20210606011337p:plain

60秒間で、MainSimulationが60回(0.5RPS × 2台)、SubSimulationが12回(0.2RPS × 1台)、それぞれ意図したとおりに実行されていました。

まとめ

AWS Batchを利用することで、OSSのGatlingを使用して、分散環境を構築することができました。

攻撃用サーバーを分散して用意することで、ハードウェアの制限を気にせず、負荷試験のシナリオを用意して実行することができるようになりそうですね。

AppSync + DynamoDBの構成でマスターデータを入れる

どうも、@jumboOrNot です。 弊社ではAppSyncのユースケースが少しずつ増え始めており、色々と調査しています。今回はその中の一つのデータストアがDynamoDBの場合のマスターデータのセットアップについて話します。

ユースケース

お題の通り。 AWSでのサーバレスアプリケーションのよくある構成の中で、リリース前にそれなりのボリュームのマスターデータを入れておきたいことがあります。またサンプルデータを入れたい時とかも出てくる話かと思います。

解決方法1 aws コマンド経由で実行

よくある手段の一つです。 サンプルにもあるように batch-write-item コマンドを使うと複数行のデータの挿入が可能です。

aws dynamodb batch-write-item --request-items file://ProductCatalog.json

この場合、入れたいデータがCSVで用意されており、サンプルにあるようにデータの構造を整形するのは手間だったので、別の方法を検討しました。

{
    "ProductCatalog": [
        {
            "PutRequest": {
                "Item": {
                    "Id": {
                        "N": "101"
                    },
                    "Title": {
                        "S": "Book 101 Title"
                    },
                    "ISBN": {
                        "S": "111-1111111111"
                    },
                    "Authors": {
                        "L": [
                            {
                                "S": "Author1"
                            }
                        ]
                    },
.......

解決方法2 CloudFormationを使ってS3経由で取り込む

公式にも記載されている方法ですね。 もし CloudFormation に慣れ親しんでいたり、S3から定常的に実行する必要があればこの手法が良いと思います。 CloudFormationを使う以外にも、AWS Data Pipeline経由で実行する方法も紹介されていますが、ちょっと手順が面倒でした。

解決方法3 スクリプトを実装して実行

  1. PHPスクリプト + AWS SDKでDynamoDBへ直接書き込む
  2. jsスクリプト + Lambda + DynamoDBで Lambda経由で書き込む etc...

いろいろな手段はありますが、今回はサーバレスでのアプリケーション構築を前提としていたため、AppSync(GraphQL) のセットアップができていたため、これ経由でinsertしてしまうのが楽だと思い、今回はこの手法で実行しました。

コマンド(Javascript + aws-sdk + aws-appsync client)--> AppSync  --> DynamoDB

この方法が今後のアプリケーションのSchemeのメンテなどに追従しやすいと考えたためです。 CSVからAppSync経由でDynamoDBにinsertする実装例を下記に記載します。

実装例

const aws = require('aws-sdk')
const AWSAppSyncClient = require('aws-appsync').default
const gql = require('graphql-tag')
const fetch = require('node-fetch')
const fs = require('fs');
const csvSync = require('csv-parse/lib/sync'); // requiring sync module

const file = 'example_master.csv';
let data = fs.readFileSync(file);

let csvData = csvSync(data);

global.fetch = fetch;
const appSyncConfig = {
// AppSyncから発行する
}

const exampleQuery = gql`
mutation MyMutation(
  $example_id: Int!,
  $created_at: Int!){
    put(
      example_id: $example_id,
      created_at: $created_at
    ) {
      example_id
    }
  }
`

const config = {
  url: appSyncConfig.graphqlEndpoint,
  region: appSyncConfig.region,
  disableOffline: true,
  auth: {
    type: appSyncConfig.authenticationType,
    apiKey: appSyncConfig.apiKey
  }
}
const options = {
  defaultOptions: {
    watchQuery: {
      fetchPolicy: 'cache-and-network'
    }
  }
}

const client = new AWSAppSyncClient(config, options)

const sleep = msec => new Promise(resolve => setTimeout(resolve, msec));

async function run(){
  try {
    const currentUnixtime = Math.floor((new Date()).getTime()/1000) // set sleep

    for (var i = 0; i < csvData.length; i++){
      let column = csvData[i]
      if (!isNaN(column[0])){
        const data = {
          example_id: Number(column[0]),
          created_at: currentUnixtime
        }
        const response = await client.mutate({
          mutation: exampleQuery,
          variables: data
        })
        await sleep(200);
      }
    }


  } catch (err) {
    console.log('error posting to appsync: ', err)
  }
}

(async ()=>{
  await run();
})();

基本的にはAppSyncに用意したQueryをjsで呼び出してCSVから渡しているだけです。 上述の batch-write-item を使うとより早く実行できますが、今回はそこまで速度も必要なかったのでそのまま実装しています。

できそうだけどできない方法

現在のDynamoDBでは PartiQL をサポートしているため、SQLライクな構文でテーブルへのアクセスが可能です。 気持ち的にはここで複数行をまるっとinsertしたいお気持ちですが、公式にもあるようにこれはサポートされておらず、上述の2つの別のやり方をする必要があります。

気をつけること

DynamoDB には CapacityUnitの設定があり、デフォルト値は結構小さく設定されています。 そのため上述のサンプルのように sleep を入れていても、これの上限を超えて処理が中断されることがあります。 そのため実行するボリュームに応じてこの設定を変更してください。 もしくは一時的に オンデマンド に設定を変更することも可能です。

CloudWatchのMetricsを見れば実行中の消費状況が見れるので、ここで判断するのが良いかと思います。 コストトレードオフなパラメータなので注意が必要です。

まとめ

まだまだ進化し続けるDynamoDBですが、複数のデータのエクスポートやインポートをしようとすると小中規模のソリューションはがもうちょっと欲しいなと思いました。

パスワードや API key を history コマンドに出さないようにする

つい先ほど今日のブログ担当だったことを思い出した塚田です。 なんか書きますと言って、思いついたのがこのシンプルなネタです。

最近では ssh することは少なくなりましたが、自分以外の人も使う踏み台サーバ(bastion server)のような 環境で、パスワードやAPI key のような クレデンシャルな情報を入力しなければならない時があります。

普通は紳士的にやらないですが、システム上は root アカウントから su してしまえば ユーザーが実行した history ログが見えてしまいます。

入力したクレデンシャルな情報はせめて過去の実行コマンドの history コマンドに出てきて欲しくない場合があります。

今回は history に出さなくなる方法を紹介します。 といっても超絶シンプルです。

HISTCONTROL=ignorespace

先頭にスペースを入力してから コマンドを入力すると、history 履歴に残らなくなります。

 export password=xxxxxxxx

それでは。

Kaggle参加してみた話

レアジョブEdTech Lab.の齋藤です。

この1ヶ月程でAPEXにハマりました。人生初FPSなので上位3チームに入れると嬉しいくらいのテンション感で戦っています。
現実ではKaggleで上位10%を目指して戦っていたので、今回はそのことを書いていきたいと思います。

Kaggleとは

ご存知の方も多いと思いますが少し説明させていただきます。
Kaggleとはデータサイエンスコンペティションを提供しているプラットフォームです。1コンペティションあたり約2〜3ヶ月程度の期間で、スコアを競い合っていきます。

www.kaggle.com

目標と参加体制

目標

冒頭に上位10%を目指したと書きました。
では、なぜ今回の目標が上位10%に設定したかというと、単純に銅メダルボーダーであるということです。

Kaggleにはメダル制度が存在しています。コンペの参加チーム総数でメダル獲得基準は若干変わりますが、順位に応じて金銀銅のメダルが与えられます。
詳細はKaggle Progression Systemを参照してください。

ということで、今回参加したコンペでは銅メダルの獲得ラインが全体の10%に入るというものだったので10%を目指しました。
1143チームが出場しているので、114位がボーダーです。

メンバー


チームは3人、全員EdTech Lab.のメンバーで出場しました。それぞれのKaggle経験は以下のような状態で開始しました。

  • 齋藤(私): 2年ぶりくらいのKaggle。
  • Kさん: 初Kaggle。
  • Cさん: しっかりKaggler。

2/3がほぼほぼKaggle初心者の状態での開始となりました。ちなみに私はチーム参加初めてです。

進行


チーム全体としては週に3時間ほど時間を作りコンペに参加しました。
ナレッジや議論、Kaggle上の気になるDiscussion・Notebookなどの情報共有をgoogle meet上で行い、その後個人作業という形で進行しました。
また、それらの情報はGithubでメンバーのみのプライベートリポジトリを作成し管理を行いました。

コンペ概要

参加したコンペ

で、どのコンペに参加したのかですが、 Rainforest Connection Species Audio Detection | Kaggleに参加しました。鳥蛙コンペと呼ばれているコンペです。

www.kaggle.com

課題

熱帯雨林に生息する鳥やカエルの種類を与えられた音声データから分類するもので、いわゆる多クラス分類のタスクになっていました。

データ

60秒の音声データが与えられました。
また、学習データは現行のアルゴリズムが判定した結果に対して

  • 専門家が正しいと判断したもの(train_tp)
  • 専門家が間違いと判断したもの (train_fp)

の2種類が与えられました。

しかし、テストデータは専門家によるアノテーション結果でした。

これにより、ラベルの分布が学習時とテスト時で大きく変わり、データシフトが起きやすい状態が発生しました。

難しい点

  • データをtrain_tpだけにするとデータ数が極端に少なくなってしまう。

  • missing labelなものやノイズが多く含まれている

  • submitすると精度が下がりがち(Validationが機能不全を起こしている)

手法

Submitしたモデル

3人の作成したsubmission用csvを混ぜたものを提出しました。

アプローチ

基本的にはmixupというaugmentation手法を使用してデータをかさ増ししていきました。
簡単な説明としては、2つのデータを混ぜて新しいデータを作成する手法になります。
嬉しい点は、ドメイン知識がなくてもデータが増やせるというところにあります。
そのため、

  • ある程度正しくアノテーションされているデータであるtrain_tp数が少ない

  • 生物の鳴き声音に対するドメイン知識を持っていない

という状態なので非常に助かりました。

詳細な数式や理論は以下の論文を参照してもらえるとわかるかと思います。

arxiv.org

予測はResNetで行いました。画像認識において有用な手法として挙げられている物です。
今回は音声データをメルスペクトログラムに変換したものを使用したので、画像系の分類タスクと捉えられるのでこの手法を取りました。

本音では、このコンペに参加したとき

ResNet34 More Augmentations+Mixup+TTA (Inference) | Kaggle というNotebookが公開されていたためとっつきやすいと思い選択しました。

余談ですが、上位解法として自力でアノテーションをしたというものがあり、”力”を感じました。 www.kaggle.com

結果

肝心の結果ですが... f:id:mag_sn:20210310200509p:plain メダル獲得ならずでした。
取得まで0.08程足りなかったというあまりにも惜しすぎる結果でした。 172位ほどshake upしましたが、全体の13.2%の位置までで止まってしまい、もう一息〜という感じでした。

振り返ってみて

今回Kaggleに参加してみての反省ですが、

  • チームのタスク管理が難しかった
  • データへの理解が足りていなかった

この2点が大きな課題だと感じました。

特にタスク管理は、チーム内の議論が手法共有などKaggleのDiscussionに寄ってしまい、お互い着手しているタスクを認識し切れていない状況を発生させてしまいました。
作業を個人に任せすぎた面もあり、作業報告が「今こういう手法でやってます」という報告になってしまい手法被りも事前に検知できなかったという点もあります。 結果的に近い手法を使用した実験を行っており、アンサンブル時にモデルのバリエーションが少ないという状態となり、スコアを上げ切れなかった原因にも繋がってしまったかなと反省です。

次回は行う作業や使用手法を事前に共有・管理できるようにしくみを作っていきたいと思います。

チーム参加でよかったことは、

  • 情報が追いやすい
  • 日本語で議論可能な場を持てるという安心感
  • モチベーションが維持される

といったものが挙げられると思います。
気になるDiscussionや論文といった情報が逐次的に共有されるので、一人での参加よりも多くの情報を追っていけるのは非常に助かる側面だと思いました。
また、それらの情報に対して日本語で議論できるので思考や理解がスムーズなのも利点だなと感じました。

最後に

今回はかなり悔しい結果に終わってしまいました。
というわけで、新メンバーを1名追加し Indoor Location & Navigation | Kaggle に参加してメダルを目指します。

www.kaggle.com

私たちの戦いはまだまだこれからです。次回の結果にご期待ください。

参考

https://connpass.com/event/204547/presentation/ https://kutohonn.hatenablog.com/entry/2021/02/18/224002 https://nonbiri-tereka.hatenablog.com/entry/2020/01/06/082921 https://qiita.com/iwashi-kun/items/fec710294e01a48c9e5d http://wazalabo.com/mixup_1.html https://openreview.net/forum?id=r1Ddp1-Rb https://www.inference.vc/mixup-data-dependent-data-augmentation/ https://www.kaggle.com/c/rfcx-species-audio-detection/discussion/220522 https://ichi.pro/merusupekutoroguramu-o-rikaisuru-277775661583955 https://www.bigdata-navi.com/aidrops/2611/

毎年恒例の iOS メジャーアップデートで iOS 14 対応した話しを振り返ります

APP/UX チームの玉置(@tamappe)です。
今回は今年に入って iOS 版レアジョブ アプリの iOS 14 対応が完了しましたので、どんな風に変更したのかについてまとめてみました。 簡単に言えば、みんな大嫌いな iOS アプリのメジャーアップデート対応が完了した話です。

毎年恒例の iOS メジャーアップデート対応

iOS アプリエンジニアは毎年この iOS のメジャーアップデートに苦しめられています。 毎年というので去年の今頃もメジャーアップデートをしていました。 当時は墓穴を掘ってしまい、ブログ記事すら書けないぐらいにメンタルをやられました。 またレアジョブに入社して最初に行ったタスクも Swift 3 系からのアップデート作業でした。エモいですね。

rarejob-tech-dept.hatenablog.com

今までSwiftのバージョンアップデートで破壊的な変更が入るたびに iOS アプリエンジニアは悲鳴を上げてきました。 なぜかというと Swift のバージョンを上げるとだいたいビルドが通らないからです。 さらに使用しているライブラリが対応できていなかったなどの要因で、端末の OS によっては UI が変わるといった謎現象も多くエンジニアはその対応に終われます。 今回もご多分に漏れず苦しめられるかとヒヤヒヤしていましたが、なんとわずか1週間で完了できてしまいました。

主な変更点

主な変更点はこちらになります。

差分内容 変更前 変更後
Swift version Swift 5 Swift 5.3
iOS のサポートバージョン iOS 11 ~ 13 iOS 12 ~ 14
Xcode Xcode 11.5 Xcode 12.3
CocoaPods 1.6.0 1.10.0

(まだSwiftPM対応はできていません。 CocoaPods を使っているのか遅れてるなぁと思う方は、是非とも弊社に入社してレアジョブアプリに SwiftPM を導入していただけると助かります。)

それでは、実際に対応した作業と苦しめられたことについてお話していきます。

移転先の Xcode のダウンロードバージョンを確定させる

iOS のメジャーアップデートをする時にいつも腰が重くなりがちなのが、アップデートする Xcode のバージョンを何にするかということです。

メジャーアップデートするたびに頭を悩ませるのはアップデート後の Xcode のバージョンです。 僕はいつも Appleアーカイブから「 Safari 上で」 Xcode をダウンロードしますが、このタスクの度に検証のためにダウンロードする Xcode のアプリケーションの数が増えてしまい Mac PC のストレージにダメージを蓄積していきます。

https://developer.apple.com/download/more/

複数の XcodeMac のローカルに保存して共存させるには Xcode アプリのリネームが必要になりますね。 なので、「Xcode_xxx.app」のアプリケーションがダウンロードするバージョン数だけ増えることになります。「xxx」はバージョンの数字を入れています。

そして、 Google Chrome から上記のアーカイブページでダウンロードするといつも解凍に失敗します。 そのため、いつも Safari からダウンロードしています。

Xcodeは10GB近くあるめちゃでかアプリケーションなので、2つほどダウンロードが完了する頃には半日が終わっていることになります。 今回はこの当時の最新版の Xcode 12.3 と 12.1 に狙いを定めてダウンロードしてみました。

結果的には Xcode 12.3 で大きなエラーが起きずにビルドが成功しましたので Xcode 12.3 で確定しました。 あまり頻繁に Xcode のバージョン上げをすることは宜しくないと思っておりますので、できるだけ最新にして長持ちできる方針を貫いています。

iOS Deployment Target の minimumを iOS 12 以降に変更する(iOS 11 サポートを切る)

無事 Xcode のダウンロードが完了したら、だいたい次に行うのが iOS のサポートバージョンを変更する作業です。 業界の定石は特にありませんが、サポートする iOS のメジャーバージョンは (最新メジャーバージョン - 2)で計算しています。 iOS 14 が最新版なら iOS 12 ~ 14 という感じです。 もっと比較的余裕のあるアプリでしたら (最新メジャーバージョン - 1)でいいかもしれません。 iOS 11 は今ではユーザーが2%ほどです。 サポートバージョンを狭くすればするほど、当然ですが運用が楽になります。古いAPIを切ることができるためです。

レアジョブアプリは iOS 14 対応をリリースしてからは iOS 11 のサポートを切ることができました。

このバージョンを変更する方法はとても簡単で XcodeDeployment Target の項目を変更すれば可能です。

f:id:qed805:20210201154908p:plain
iOS suport version

これで iOS 12 以上からになります。

Swift 5.0 から 5.3 へアップデート

次に頑張りたかったことが、 Swift バージョンの変更でした。 Xcode 11.5の時は Swift 5.0 でしたが Xcode12.3 では Swift 5.3 が使えますのでこちらに変更しました。 また、レアジョブアプリはライブラリ管理ツールに CocoaPods を使っています。その関係で、 Pods の Swift バージョンも Swift 5.3 に変更する必要がありました。

そのため、 Podfile ファイルで次のように変更しました。

post_install do |installer|
  installer.pods_project.targets.each do |target|
    target.build_configurations.each do |config|
      config.build_settings['SWIFT_VERSION'] = "5.3"
      config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '12.0'
    end
  end
end

これでターミナルから pod update でライブラリの Swift のバージョンが変わります。

CocoaPods のアップデート (1.6.0 から 1.10.0)

ですが、ここではそう上手いこといきませんでした。 使用しているライブラリによっては Pods のバージョンが足りないと言われてしまいました。

そのため、 CocoaPods のバージョンを上げる必要がありました。 レアジョブアプリは Pods のバージョン管理は Gem で行っていましたので、 Gemfile を修正してから

bundle install --path vendor/bundle

を実行して Pods のバージョンをあげました。 これで再度、 Pods の更新を行うと無事ライブラリのバージョンアップが完了しました。

一部 UI のレイアウト崩れの改修

これでビルドが成功してめでたしめでたしであれば非常に楽なのですが、 iOS のメジャーアップデートの作業の本番は大抵ここからスタートになります。 できるだけ UI のレイアウトが崩れていないことを願いながらシミュレーターなり実機で画面を確認していきますが、中には画面が開こうとするとアプリがクラッシュしたり UI がガッツリ崩れていたりします。

ここで予め用意しておいたテスト前に実施するテスト項目書を使ってマニュアルで全ての画面を表示させながらアプリを操作して挙動を確認しました。 今回は一画面だけ UI が崩れていましたのでその部分が治るように修正しました。

まさかの iOS 13 端末では UI が崩れず iOS 14 でのみ崩れる不具合で、改修後も複数のシミュレーターを起動してみたりと確認作業にかなり時間を取られました。

総括

そんな感じでレアジョブアプリは無事に iOS 14 対応が完了しました。 レッスンルームも綺麗になりましたし、前回の投稿からユーザーが劇的に増えましたので是非レアジョブアプリを使っていただければと思います。