kurosame’s diary

フロントエンド中心です

Firebase の Functions から Firestore への連携が(たまに)失敗する件の対応

あるシステムで以下のような処理を行っている

  1. Firebase の Storage にファイルをアップロード
  2. そのファイルを Functions でダウンロードして色々処理
  3. 加工した情報を Firestore に保存

Firestore への保存処理は Functions でadmin.firestore().collection('...').doc(fileName).set(...)のようにset関数を使って行っている

この Firestore への保存処理が成功する時と失敗する時があった

Functions が原因の可能性が高いと思ったが、エラーも出ておらず、成功する時と失敗する時があるのでちゃんとした再現が難しく、調査に難航した

Firebase は Spark プラン(無料のやつ)
Firebase プロジェクトのリージョンはnam5 (us-central)
Firestore のリージョンはnam5 (us-central)

やったこと、やろうとしたこと

  • 何かプランの制限にかかっていないか確認
  • 試しに Functions のリージョンを変えてみる
  • Firebase リソースのリージョンをすべて東京にする
  • Firestore へ反映されていなかったらリトライする

何かプランの制限にかかっていないか確認

以下を確認
https://firebase.google.com/docs/functions/quotas

とくに大丈夫そう

試しに Functions のリージョンを変えてみる

Functions はデフォルトではus-central1で実行される
東京リージョンに変えて実行してみた

functions
  .region('asia-northeast1')
  .storage.object()
  .onFinalize(...)

Functions のダッシュボード上で以下に変わっていればオッケー f:id:kurosame-th:20200515192533p:plain

なぜか Functions のリージョンを東京に変えたら失敗しなくなったっぽい(何回か試しただけだけど)
でも Storage も Firestore もリージョンはnam5 (us-central)なので、Functions の実行環境として 1 番近いのはus-central1である

この検証結果は忘れることにした

Firebase リソースのリージョンをすべて東京にする

Firebase プロジェクトのリージョンは後から変更できない
変えるならば新規プロジェクトを作り、全データ移行および設定し直しをする必要がある

かなり面倒だからやめた。。

ちなみに Firebase プロジェクトをasia-northeast1リージョンに変更しても、Firebase の主要リソースはすべて利用できる
また、自分が調べた限りasia-northeast1リージョンで不利になるのは以下だけ

  • Firebase Hosting で Functions を使って動的コンテンツを配信するのはus-central1のみをサポート
  • Realtime Database は常にus-central1になる

参考
https://firebase.google.com/docs/functions/locations?hl=ja#background

Firestore へ反映されていなかったらリトライする

結局、失敗したらリトライ処理をするプログラムを書くことにした

const doSet = async (): Promise<void> => {
  admin
    .firestore()
    .collection('...')
    .doc(fileName)
    .set(...)
  await new Promise(resolve => setTimeout(resolve, 120000)) // Sleep for 2 minutes
  return admin
    .firestore()
    .collection('...')
    .doc(fileName)
    .get()
    .then(doc => {
      if (!doc.exists) return Promise.reject()
      const data = doc.data()
      return newData === data ? Promise.resolve() : Promise.reject()
    })
    .catch(() => Promise.reject())
}

けっこう省略してるが、doSet関数は以下の処理を行っている

  1. admin.firestore().collection('...').doc(fileName).set(...)で Firestore のドキュメントを登録/更新
  2. 2 分スリープ
  3. admin.firestore().collection('...').doc(fileName).get()で Firestore のドキュメントを取得
  4. 取得したドキュメントをチェック
    1. ドキュメントが存在しない、または更新されていない場合はPromise.reject()を返す
    2. ドキュメントが登録/更新されている場合はPromise.resolve()を返す

doSetは以下のように呼ぶ

doSet()
  .catch(doSet)
  .catch(doSet)
  .catch(() => console.error(`Document not updates`))

最大 3 回doSetをコールする
3 回目のコールでdoSetPromise.reject()を返せば、エラーログを出して処理終了
いずれかのdoSetPromise.resolve()を返せば、キャッチされないので処理終了

本当は Functions でエラー時にアラートを仕込めたらよいが Firebase コンソール上ではできなそう
GCP の Stackdriver Logging(有料)を使って色々設定すればできるかも

また、注意点として、Functions のデフォルトのタイムアウト時間は 60 秒なので以下のように変更しておく

functions
  .runWith({ timeoutSeconds: 540 })
  .storage.object()
  .onFinalize(...)

540 秒(9 分)が最大値
Functions のダッシュボード上で以下に変わっていればオッケー f:id:kurosame-th:20200515192607p:plain

また、runWithオプションで CPU のコア数やメモリも増やせる

参考
https://firebase.google.com/docs/functions/manage-functions?hl=ja#set_timeout_and_memory_allocation

さいごに

Firebase で最初にプロジェクトを作ったとき、ちゃんと東京リージョンにしておけば、今回の問題は起きなかったかもしれない

Functions のプログラム全体は以下
https://github.com/kurosame/glossary/blob/master/functions/src/index.ts

Flutter の環境構築

はじめに

インストール環境

参考サイト
https://flutter.dev/docs/get-started/install/macos

Flutter SDK のインストール

※ 以下の手順を行うより、追記した Homebrew でのインストールが1番楽

cd ~/Dev/Native-Apps
git clone https://github.com/flutter/flutter.git -b stable

config.fishに以下を追記

set -x PATH $HOME/Dev/Native-Apps/flutter/bin $PATH
source ~/.config/fish/config.fish

flutter doctorを実行

flutter doctorは環境構築完了に向けて足りてないことを教えてくれる(すげー!)
ほぼ依存ライブラリのインストールやアップデートになる

主にやったこと

  • Android Studio から SDK Manager を起動し、Android SDKAndroid BuildTools を最新にする
  • flutter doctor --android-licensesを実行し、すべてのライセンスを承諾
  • sudo gem install cocoapods
  • 利用する IDE で Flutter プラグインをインストール

flutter create [Project Name]を実行

Android の設定

  1. Android Studio を起動
  2. Configure -> AVD Manager
  3. 適当なデバイスを追加
  4. flutter emulatorsを実行し、さきほどのデバイスがあればオッケー
  5. バイスが起動してる状態でflutter runを実行
  6. カウンターアプリが表示されたらオッケー

iOS の設定

  1. Xcode を起動
  2. Xcode -> Open Developer Tool -> Simulatorを選択
  3. バイスが起動してる状態でflutter runを実行
  4. カウンターアプリが表示されたらオッケー

複数デバイスを起動した状態でflutter runするときは-d <deviceId>でデバイスを指定するか-d allで全デバイスを指定するかのオプションが必要になる

追記

IDE を起動せずに Emulator を起動

~/Library/Android/sdk/emulator/emulator -avd [対象のAVD名] # Android
open -a Simulator # iOS

追記

Homebrew で Flutter をインストール

brew install flutter
which flutter
flutter doctor

@nuxtjs/moment の型定義

Vuex の型定義を拡張して、@nuxtjs/moment の型を追加してあげようっていうだけの記事です

まずは前提として、Nuxt.js では store ディレクトリ配下が Vuex になっており、
store 配下にファイルを作成して、そのファイル内で actions オブジェクトを export すれば、
ファイル名/ActionTypeという指定で Vue のコンポーネントとかからアクションを dispatch できる

この actions オブジェクト内で this を参照すると、this はStore<S>という型になっている

今回の問題は@nuxtjs/momentStore<S>に含まれていないので、以下のように型エラーになってしまう

f:id:kurosame-th:20191025183646p:plain

面倒なら this を any にキャストしても良いと思うが、一応以下の型定義をプロジェクトルートとかに置けば、エラーは無くなる

// @types/@nuxtjs-moment.d.ts
import { Moment, MomentFormatSpecification, MomentInput } from 'moment'

declare module 'vuex/types/index' {
  interface Store<S> {
    $moment(
      input?: MomentInput,
      format?: MomentFormatSpecification,
      language?: string,
      strict?: boolean
    ): Moment
  }
}

追記

Vue コンポーネントから@nuxtjs/momentを参照する場合は、以下も追加しておく

// @types/@nuxtjs-moment.d.ts
import { Moment, MomentFormatSpecification, MomentInput } from 'moment'

declare module 'vue/types/vue' {
  interface Vue {
    $moment(
      input?: MomentInput,
      format?: MomentFormatSpecification,
      language?: string,
      strict?: boolean
    ): Moment
  }
}

今回実装したコード

GitHub - kurosame/event-search: Event search App with Nuxt.js

React + Firebase Authentication

以下の React で作ったアプリに Firebase Authentication を導入してみようと思う
Glossary

今回実装したコードはこちら
GitHub - kurosame/glossary: Glossary App using React


Firebase 側の設定

Firebase の Authentication からログイン方法を設定をクリック

f:id:kurosame-th:20190917170030p:plain

Google 認証を有効にして保存

f:id:kurosame-th:20190917170551p:plain


Firebase の初期設定

npm i firebase

Firebase のサービスを使う際は以下の設定が必要
下記の情報は Firebase からコピペできる
ちなみに隠してるけど、これらの情報は公開してもセキュアです

// firebase/config.ts
const config = {
  apiKey: '...',
  authDomain: '...',
  databaseURL: '...',
  projectId: '...',
  storageBucket: '...',
  messagingSenderId: '...'
}

export default config

Firebase を初期設定する

// firebase/index.ts
import firebase from 'firebase/app'
import 'firebase/auth'
import config from '@/firebase/config'

const firebaseApp = firebase.initializeApp(config)
export const auth = firebaseApp.auth()

ログイン画面を実装

ログインフローは全て FirebaseUI に任せる
FirebaseUI の React ラッパーが公式であるので、こちらを使って実装する

npm i react-firebaseui

以下は FirebaseUI の Config 設定
ログインの表示方法や認証後の遷移先、認証の種類を定義する

// firebase/ui-config.ts
import firebase from 'firebase/app'
import 'firebase/auth'

const uiConfig = {
  signInFlow: 'popup',
  signInSuccessUrl: '/',
  signInOptions: [firebase.auth.GoogleAuthProvider.PROVIDER_ID]
}

export default uiConfig

以下はログイン画面
Redux 周りの説明は省略します
componentDidMount ライフサイクル関数で Firebase の onAuthStateChanged オブサーバー関数を使って、user を取得している
user が取れればログイン済と判断し、null の場合は未ログインと判断している
そして、render 関数内で Store に保持している isLogin を使って、レンダリング有無を判断している
今回 isLogin を他のコンポーネントで使うため、Redux を使って Store に保持したが、他で使う用途が無ければ Local State で良い

また、firebase.auth().currentUserを使っても同様にログイン有無の判断ができるが、タイミングによってはログイン後でも null を返す場合がある
firebase.auth()が初期化されてないタイミングで currentUser を呼び出すと null になるらしい
推奨されている解決方法は onAuthStateChanged 関数のコールバックで user を受け取り、処理を行うことです
currentUser を使うのは、onAuthStateChanged 関数の処理の後が良いと思う

// Login.tsx
import React from 'react'
import StyledFirebaseAuth from 'react-firebaseui/StyledFirebaseAuth'
import { connect } from 'react-redux'
import { bindActionCreators, Dispatch } from 'redux'
import uiConfig from '@/firebase/ui-config'
import { auth } from '@/firebase/index'
import { States } from '@/modules/states'
import { LoginActions, LoginState, setIsLogin } from '@/modules/login'

interface Props {
  state: { login: LoginState }
  actions: LoginActions
}

export class Login extends React.PureComponent<Props> {
  componentDidMount(): void {
    auth.onAuthStateChanged(user =>
      this.props.actions.setIsLogin({ isLogin: !!user })
    )


  // ログイン済であればnull、未ログインであればStyledFirebaseAuthを返す
  public render(): JSX.Element | null {
    if (this.props.state.login.isLogin) return null
    return <StyledFirebaseAuth uiConfig={uiConfig} firebaseAuth={auth} />
  }
}

export default connect(
  (states: States) => ({ state: { login: states.login } }),
  (dispatch: Dispatch) => ({
    actions: {
      setIsLogin: bindActionCreators(setIsLogin, dispatch)
    }
  })
)(Login)

動作確認

アプリを実行すると、以下のよく見る Google ログインが表示されるので、Google アカウントを使ってログインする

f:id:kurosame-th:20190918173255p:plain

ログイン後、Firebase コンソールのプロジェクトで Authentication タブを確認すると、ログインしたユーザが表示されていると思う
また、onAuthStateChanged 関数を使って、Store に保存した isLogin を見ると、ログイン済は true、未ログインは false が設定されていると思う

Python と VSCode の快適な開発環境を考える

2023/06追記

パッケージ管理は、Ryeが良さそう
Rye は Flask の作者が開発している

brew install rye

プロジェクト初期化

rye init .

依存パッケージ追加

rye add [パッケージ名]
rye sync

実行

rye run python main.py

最近 Go の開発環境を VSCode で構築したので、Python の開発も IntelliJ から VSCode に移行しよう
Go と違って PythonIntelliJ の Community 版でもストレス無く開発してたが、複数 IDE だと面倒なので開発環境を VSCode に統一したい


Pipenv

Pipenv はパッケージ管理と実行環境(仮想環境)の構築を行ってくれる

brew install pipenv
export PIPENV_VENV_IN_PROJECT=true
pipenv install
pipenv --venv

デフォルトだと仮想環境は~/.local/share/の下に作られる
色々不便なので、PIPENV_VENV_IN_PROJECT=trueを設定してプロジェクト直下に作られるようにしている

VSCode の settings.json に以下を追加して、再起動

"python.pythonPath": "${workspaceFolder}/.venv/bin/python",

VSCode

Python 拡張プラグインをインストールする

おそらく VSCode の Notification で Lint やコードフォーマッターのインストールを促すメッセージが出ているので、インストールする
上記の Pipenv の設定が済んでいれば Pipenv でインストールされる、Pipenv を使ってなければ pip でインストールされる

普通にコマンドでインストールする場合は以下

pipenv install pylint --dev
pipenv install black --dev

コードフォーマッターはblackにした

"python.formatting.provider": "black"

Go と VSCode の快適な開発環境を考える

2022-05-23

以下の GOPATH と dep を使ったパッケージ管理は古いので更新

Go をインストールもしくは、最新にする

brew install go # Go 1.18

他の依存管理ツールは不要

cd bots/rss
go mod init github.com/kurosame/bots-go/bots/rss

go.modが作成される

import に依存パッケージを記載

import (
    "github.com/joho/godotenv"
    "github.com/slack-go/slack"
)
go mod tidy

go.sumが作成される

go.modはパッケージのバージョンやパスが記載されている
go.sumはパッケージのチェックサムが記載されている(パッケージをインストールした時と完全に同じことを担保する)

go.modgo.sumGitHub 管理した方がよい
https://github.com/golang/go/wiki/Modules#releasing-modules-all-versions

複数のバージョンのGoをインストール
https://go.dev/doc/manage-install

go install golang.org/dl/go1.16.15@latest
~/go/bin/go1.16.15 download
~/go/bin/go1.16.15 version

~/go/binを PATH に通しておくとよい


IntelliJ で Go を書こうと思ったが、Community 版なので Go プラグインが古いのしか無かった
なので VSCode で書こうと思って環境構築しました
けっこう色々必要かなと思って構築したので、メモっておきます


goenv

Go のバージョン管理を行うツール
Go のバージョンを簡単に切り替えることができる

brew install goenv

以下を追加

# ~/.config/fish/config.fish

# goenv
set -x PATH $HOME/.goenv/shims $PATH
source ~/.config/fish/config.fish
goenv install 1.11.4
goenv global 1.11.4
go version

direnv

ディレクトリごとに環境変数を切り替える
例えば GOPATH をディレクトリごとに変えることができる
⇒ 私の場合はghqを使っているので、ghq 配下のリポジトリごとに GOPATH を変えれるのが便利でした

brew install direnv

以下を追加

# ~/.config/fish/config.fish

export EDITOR=vim # これを設定してないと`direnv edit`でエラーになる

# direnv
eval (direnv hook fish)
source ~/.config/fish/config.fish
direnv edit .

EDITOR 環境変数で設定したエディタが開くので、以下を追加

export GOPATH=$(pwd)

GOPATH を今のディレクトリにするって感じです


dep

公式のパッケージ管理ツール

brew install dep

dep$GOPATH/src/projectというディレクトリ構成にしないといけないらしい
※ project は任意の名前を付けてください

mkdir src/default
cd src/default
dep init

結果 GOPATH 配下は、以下のようなディレクトリ構成になる
(なんかキモい構成だけど我慢しよう…)

.
├── .envrc
├── .gitignore
├── pkg
│   └── dep
│       └── sources
└── src
    └── default
        ├── Gopkg.lock
        ├── Gopkg.toml
        └── vendor

pkg/depdep のキャッシュで vendor に実際使ってるパッケージが入る
src/default 配下が作業ディレクトリになる

以下のように外部パッケージが必要な import があれば

import (
    "github.com/pkg/errors"
)

以下を実行するだけで、依存解決しつつ vendor ディレクトリにインストールしてくれる

dep ensure

依存を確認する場合は、Gopkg.lockを見るかdep statusを実行すると良いです


VSCode

とりあえずGo 拡張プラグインをインストールする
このプラグインは色々外部パッケージが必要なので、go getでインストールする
(たぶん VSCode の Notification でインストールを促すメッセージが出てると思います)

npm 慣れしてる私からすると Lint やコードフォーマッターなどの開発専用系のパッケージも依存管理するものだと思っていたが、dep がたぶんプログラムに import したパッケージのみを管理するようなので、それに従って開発専用系のパッケージはgo getでインストールする

また、dep 管理しているパッケージは認識してくれないので、VSCode の settings.json に以下を追加して、再起動

"go.gopath": GOPATHを設定

複数リポジトリで settings.json を使い回すことを想定していれば、"go.gopath": "${workspaceFolder}"などにした方が良いでしょう


最終的に、以下のようなディレクトリ構成になった
(なんかさらにキモい構成になった気がする…)

.
├── .envrc
├── .gitignore
├── bin -> `go get`したやつのバイナリ
├── pkg
└── src
    ├── default
    │   ├── Gopkg.lock
    │   ├── Gopkg.toml
    │   └── vendor
    ├── github.com -> `go get`したやつ
    └── golang.org -> `go get`したやつ

Go 開発はかなり久しぶりなので、開発しながら良い構成を考えていきたいと思います

今回の作業リポジトリgithub.com

また、これから作るアプリケーションのディレクトリ構成は以下を参考にしようかなと思ってます github.com

webpack + TypeScript 環境でのバンドル速度改善

会社の webpack が遅くなってきたので、TypeScript 周りでバンドル速度が改善できるか調べてみました
そして、以下の記述に見つけて、バンドル速度改善をする上で割と重要なオプションな気がしたので実装してみようと思います
GitHub - TypeStrong/ts-loader: TypeScript loader for webpack

今回実装したコードの結果は以下にあげてます github.com

では、簡単ですが実装を説明していきます


実装

ビルドプロセスを並列化する方法として、HappyPackthread-loaderのどちらかを使う方法があるそうです
ただ、HappyPack の GitHub ドキュメントを見ると、HappyPack はメンテナンスモードに入るそうなので、今回は thread-loader を使うことにします

以下のように他の loader の前に記述することで、以降の loader の処理を並列化します
thread-loader は複数の Worker スレッドで loader を実行することで並列化を実現しています

// webpack.config.js
module: {
  rules: [
    {
      test: /\.tsx?$/,
      use: [
        {
          loader: 'thread-loader',
          options: {
            workers: require('os').cpus().length - 1
          }
        },
        'babel-loader',
        'ts-loader',
        'tslint-loader',
        'stylelint-custom-processor-loader'
      ],
      exclude: /node_modules/
    }
  ]
}

thread-loader を使う設定ができたら、次は ts-loader の happyPackMode をtrueにします
happyPackMode をtrueに設定すると transpileOnly オプションが暗黙的にtrueになるみたいです
ts-loader は JS へのトランスパイルと静的型チェックを行ってくれるのですが、transpileOnly オプションをtrueにすると JS へのトランスパイルのみを行います
静的型チェックを行わない場合や他のプラグインで代替する場合は、transpileOnly オプションをtrueにすることでコンパイル速度を上げることができます

// webpack.config.js
module: {
  rules: [
    {
      test: /\.tsx?$/,
      use: [
        {
          loader: 'thread-loader',
          options: {
            workers: require('os').cpus().length - 1
          }
        },
        'babel-loader',
        {
          loader: 'ts-loader',
          options: {
            happyPackMode: true
          }
        },
        'tslint-loader',
        'stylelint-custom-processor-loader'
      ],
      exclude: /node_modules/
    }
  ]
}

次にFork TS Checker Webpack Pluginを使って型チェックを行います
checkSyntacticErrors オプションをtrueにしてますが、HappyPack や thread-loader を使用している場合は必須の設定のようです
Fork TS Checker Webpack Plugin は Syntax エラーと Semantic エラーの両方をチェックする機能を備えているのですが、checkSyntacticErrors オプションはデフォルトはfalseなので Semantic エラーのみをチェックします
しかし、transpileOnly オプションがtrueでも Syntax エラーはチェックするので、checkSyntacticErrors オプションをtrueにする必要は本来は無いのですが、happyPackMode をtrueにした場合は、Syntax エラーはチェックされないみたいなので、checkSyntacticErrors オプションをtrueにする必要があります

分かりづらいですね、、しかも今回やろうとしてる速度改善とはあまり関係がありません
でもとりあえず設定しておいた方が良さそうです
ちなみにこのことは以下の checkSyntacticErrors の説明の所に書いてました
GitHub - Realytics/fork-ts-checker-webpack-plugin: Webpack plugin that runs typescript type checker on a separate process.
翻訳間違ってたらすみません

// webpack.config.js
const ForkTsChecker = require('fork-ts-checker-webpack-plugin')

module: {
  rules: [
    {
      test: /\.tsx?$/,
      use: [
        {
          loader: 'thread-loader',
          options: {
            workers: require('os').cpus().length - 1
          }
        },
        'babel-loader',
        {
          loader: 'ts-loader',
          options: {
            happyPackMode: true
          }
        },
        'tslint-loader',
        'stylelint-custom-processor-loader'
      ],
      exclude: /node_modules/
    }
  ]
},
plugins: [
  new ForkTsChecker({ checkSyntacticErrors: true })
]

実装は以上です


検証

とりあえず実行
f:id:kurosame-th:20190421150748p:plain
約 15 秒
あまり速くなってない。。

試しに thread-loader の Worker の数を 1 つにして実行
f:id:kurosame-th:20190421151046p:plain
こっちの方が倍速いじゃないか!

thread-loader の Worker の数を 2 つにして実行
f:id:kurosame-th:20190421151028p:plain
Worker の数を増やす度に遅くなってる。。😇

原因として今回検証に利用したReact Boilerplateはかなり小規模な検証環境だったので Worker の起動やプロセス間通信にかかるオーバーヘッドの方が大きかったのかなと思います
会社のプロジェクトであれば、そちらはかなり肥大化しているので、今度試してみます