Posts for: #2022/08

休日の本番障害

夕方から寝ていて何度か起きたものの、そのままずっと寝ていた。あまりないことなんだけど、珍しくたくさん眠れた。

ストレッチ

今日の開脚幅は開始前160cmで、ストレッチ後163cmだった。計測の仕方がややいい加減だった気もしたが、先週より少しよくなったということにしておく。右腰の張りが強いのと肩が前に入りがちなので肩を開いて姿勢を保つように心がけるとよいとアドバイスをいただいた。もう通い始めて1年半ぐらい経つ。トレーナーさんも大半が入れ替わっていて通い始めたときに話しかけてくれた私が知っているトレーナーさんはほとんどいない。1年半も経つと人は変わっていくなというのを実感している。私の最初のトレーナーさんは社内制度で別の店舗の助っ人に行っているのでいなくなった人たちが辞めているわけでもないとは思うけど、1-2年で人が入れ替わってもサービスは継続していかないといけないし、会社ってそういうものだなと実感する機会でもある。

aws インフラの調子が悪い?

1-2週間ぐらい前からテスト環境を含めると複数回発生している eks クラスターの障害 がたまたま土曜日の夜という休日に発生した。いま eks クラスターのインフラの振る舞いを把握しているのは私だけなので、気付いてから指示を出して問題が発生している k8s ノードの削除 (ec2 インスタンスの削除) で復旧させるワークアラウンドで復旧させた。私は本番環境にアクセスできないので詳しい調査はできない。状況を正しく把握できてはいないけれど、k8s ノードが死んだり生き返ったりする不安定な状況に発生しているらしく、k8s ノードを削除して新規に作り直すと復旧することがわかっている。NotReady と Ready を繰り返したりしてアプリケーションの振る舞いが不安定になる。NotReady,SchedulingDisabled になれば、おそらく drain して k8s ノードが入れ替わってくれるのだけど、そうならない不安定な状況があるみたい。これ以上の調査は aws のサポートに問い合わせないとわからない。

mvp とその目的

0時に寝て6時に起きた。

隔週の雑談

顧問のはらさんと隔週の打ち合わせ。先日の勉強会で聞いた mvp の考え方 について気になったので相談してみた。はらさんと相談していて mvp とは一般論としてこうだと語ることはできないということが理解できた。大事なのは mvp でやりたい目的であって、その目的によって mvp が何になるかは変わってくる。

  • コンシューマー向け (toC) と業務向けの (toB) でアプリケーションは全く異なる
    • toC 向けはニーズや要件がまったくわからない
    • toB 向けはニーズや要件が明確にわかっている
  • mvp が何になるかは目的によって異なる
    • toC 向けはニーズや要件を把握するのが目的になる場合がある
      • PMF (Product Market Fit) という概念もある
    • toB 向けはニーズや要件に合致するものかどうかを確認するのが目的になる

あと フリーランス協会 という団体があるのを教えていただいた。フリーランスは会社員と比較して守られていない箇所がたくさんある。例えば、取引先とトラブルになって損害賠償が発生するとかがある。うちの会社に限って言えば、役員は基本的に労災保険の適用外になる。この仕組みを見直そうといった取り組みや制度があるという記事もみかけたりはする。そういった保険やら福利厚生やらいろんなものをパッケージングして比較的安い金額で提供してくれるらしい。はらさんによると、なぜ○○協会といった団体がたくさん作られるかというと、団体では受けられるが、個人では受けられないサービスがあるからだという説明をされていた。

むきなおり

チームで改善のための会議があった。会議のホストがいなくて、誰も何も準備をせずに会議が始まり、課題は何なのかを会議中に相談して決めるみたいな、これまでもそうだったようにだらだらした会議の始まり方をした。私はすぐにいなくなる人間なのであまり今後の方針についてはコメントしないようにしている。議論の行方だけを聞いていた。ストーリーポイントとベロシティの運用が混乱をもたらし始めていて、PO がベロシティは納期の対する進捗度合いを把握するために計測していると発言していて取り返しがつかなくなっているように感じた。いつまでに何ができかるかの期日を出さないといけないという使命感からストーリーポイントをスケジュールと同視ことから抜け出せていない。周りの知人と話したり、私自身の解釈としてもストーリーポイントとベロシティで把握できるのはチームの成熟度や成長の度合いだけになる。1年前よりもベロシティが増えているのであればチームは成長したと言える。言わばたったこれだけのことしか分からないし、成長の度合いに関すること以外に使うべきではない。

ストーリーポイントの誤解と誤用は、特にプロセスがチームから部署、企業へとスケールして行くときに、時間をかけて増幅されていきます。これらの誤解と誤用を正すのは、時間と労力を要します。

ストーリーポイントを使うのをやめよう で書かれているように、ストーリーポイントという抽象化された数値が独り歩きして、自分たちの都合のよい指標になると考え始めている。この記事によると、これが周りに広がっていくにしたがって混乱が増幅していくように書かれている。いま正にそういう雰囲気をみていて実感した。

spring boot におけるプロキシ実装

0時に寝て7時に起きた。

spring boot における簡易的なミドルウェアの実装

bff (backend for frontend) の web api サーバーから別の web api サーバーのエンドポイントを呼び出す処理を書いていてただプロキシする処理のためだけに controller を書くのも面倒だなと気付いた。controller よりも低いレイヤで raw request を扱うにはどうしたらいいかを調べてみたら spring の Filter を使うのが最も簡単そうにみえた。その spring の Filter の1つである OncePerRequestFilter を使って簡易的なプロキシの処理を実装してみた。OncePerRequestFilter はリクエストに対して1度だけ呼ばれることが保証された Filter になる。これらのドキュメントがどこにあるかわからなかったので baeldung のチュートリアル What Is OncePerRequestFilter? をみた方が早いかもしれない。

Filter が複数あるときは実行順序を指定したいときは Order というアノテーションで制御できる。

@Component
@Order(30)
public class MyFilter extends OncePerRequestFilter {
  ...
}

OncePerRequestFilter の doFilterInternal メソッドをオーバーライドすれば任意のミドルウェアっぽい処理を実装できる。

    @Override
    protected void doFilterInternal(
        HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
        throws ServletException, IOException {
        // ここに任意の前処理を実装する
        filterChain.doFilter(request, response);
        // ここに任意の後処理を実装する
    }

あとは HttpServletRequest と HttpServletResponse を直接操作してリクエストから必要な情報を取り出して、クライアントでリクエストして返ってきたレスポンスをそのまま返してあげればよい。簡単に実装したものが次になる。クライアントには 以前に少し調べたことを書いた WebClient を使っている。WebClient じゃなければ、もう少しシンプルに書ける気もする。

@Component
@Order(30)
public class RequestForwardFilter extends OncePerRequestFilter {
    @Autowired
    private List<RequestForwardMatcher> matchers;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        var matcher = this.getMatcher(request);
        if (matcher.isEmpty()) {
            filterChain.doFilter(request, response);
            return;
        }
        writeResponse(response, this.forwardRequest(request, matcher.get().getWebClient()));
    }

    private Optional<byte[]> forwardRequest(HttpServletRequest request, WebClient webClient) throws IOException {
        var method = HttpMethod.resolve(request.getMethod().toUpperCase());
        var originalUri = request.getRequestURI();
        var uri = this.getUri(originalUri, request.getQueryString());
        var spec = this.getSpec(method, webClient, uri, request.getReader());
        return spec.retrieve().bodyToMono(byte[].class).blockOptional();
    }

    private void writeResponse(HttpServletResponse response, Optional<byte[]> bytes) throws IOException {
        if (bytes.isEmpty()) {
            return;
        }
        var stream = response.getOutputStream();
        stream.write(bytes.get());
        stream.flush();
        stream.close();
    }

    private byte[] getRequestBody(BufferedReader reader) {
        return reader.lines().collect(Collectors.joining()).getBytes();
    }

    private String getUri(String originalUri, String queryString) {
        var path = originalUri.replace("/api", "");
        if (StringUtils.hasLength(queryString)) {
            return String.format("%s?%s", path, queryString);
        }
        return path;
    }

    private WebClient.RequestHeadersSpec<?> getSpec(
            HttpMethod method, WebClient webClient, String uri, BufferedReader reader) {
        switch (method) {
            case GET:
                return webClient.get().uri(uri);
            case POST:
                return webClient.post()
                        .uri(uri)
                        .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
                        .bodyValue(this.getRequestBody(reader));
            case PUT:
                return webClient.put()
                        .uri(uri)
                        .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
                        .bodyValue(this.getRequestBody(reader));
            case DELETE:
                return webClient.delete().uri(uri);
            default:
                throw new RuntimeException(String.format("Unsupported HTTP method: %s", method));
        }
    }

    private Optional<RequestForwardMatcher> getMatcher(HttpServletRequest request) {
        for (var matcher : this.matchers) {
            if (matcher.match(request)) {
                return Optional.of(matcher);
            }
        }
        return Optional.empty();
    }
}

matcher のサンプル実装。

public class RequestForwardMatcher {
    private static final String ASTERISK = "*";

    private final Pattern pattern;
    private final List<String> methods;
    private final WebClient webClient;

    public RequestForwardMatcher(String pattern, List<String> methods, WebClient webClient) {
        this.pattern = Pattern.compile(pattern);
        this.methods = methods;
        this.webClient = webClient;
    }

    private boolean matchMethod(String method) {
        if (this.methods.contains(ASTERISK)) {
            return true;
        }
        return this.methods.contains(method);
    }

    public boolean match(HttpServletRequest request) {
        if (!matchMethod(request.getMethod())) {
            return false;
        }
        return this.pattern.matcher(request.getRequestURI()).matches();
    }

    public WebClient getWebClient() {
        return this.webClient;
    }
}

たまには画面作り

1時に寝て6時に起きた。

リファクタリングとインフラ移行

ここ2週間ほどリファクタリングやらインフラ変更やらをしてきて、来週からまた新しい施設がサービスインするので区切りとしてリファクタリングは一旦終わりにする。今日がその集大成となるインフラ移行も含めた本番リリースだった。インフラ移行するときはなにかしら障害が起きる前提で待機しているものの、今日もすんなりと意図した通りに移行できて、してやったりではあるものの、モノ足りなさで拍子抜けしてしまった。また昨日から社内 wiki にも minikube の使い方、k8s cronjob の設計、バッチ処理の設計と実装についてドキュメントなどを書いていた。いままですべて私が1人で担当していたものを他メンバーでも作業できるようにドキュメント化した。近いうちにいなくなるので引き継ぎのドキュメントにもなる。

nuxt で画面作り

ここ最近2種類の web api の機能を作ったのでその管理画面も2つ作る必要がある。私はフロントエンド開発の素人なので他のメンバーが作ってくれないかと声をかけてはいたけど、みんな忙しいようなので私が作ることにした。今週は nuxtjs の新規画面の開発をがんばってみようと思う。既存のソースを読む限りはそんなに複雑ではなさそう。素人が雰囲気で実装しても動くんじゃないかと思っている。ソースコードを読んでいて url 設計はめちゃくちゃだし、一覧画面にはページング機能も実装されていない。素人がソースを読んで基本的な骨子や機能が正しく実装されてないことがわかってしまうのは品質レベルとしてなにかがおかしい。圧倒的低品質と呼ぶのか、こんなことが起こってしまうのはよい開発文化がないせいなのだろうと考えている。

リーン思考の考え方

1時に寝て6時に起きた。夜中に3回ぐらい起きたかな。夜あまり眠れない。

リーン思考を学ぶ勉強会

スクラムマスター主催の勉強会があった。スクラムマスターは折に触れてリーン思考が大事という発言をするので気にはなっていた。リーンという概念はトヨタ生産方式から触発されている。リーン思考はフロー効率の方を重視するらしい。

  • フロー効率: 価値を素早く提供する
  • リソース効率: リソースが遊び無く稼働している

どちらも効率の話しをしているが、なんの効率をあげるかが異なる。i/o におけるレイテンシとスループットの関係に似ている。レイテンシを上げればスループットは下がるが、スループットを上げればレイテンシは下がるという反比例の関係となる。おそらくフロー効率とリソース効率の話しがトヨタから出ているのであれば、工場の稼働率の話しとも関連があると考えられるのでお互いにトレードオフの関係が成り立つのではないかと推測する。リーン思考は小さく作って育てていくことを指していて、同義語として mvp, イテレーティブ、バーティカルスライドなどがある。スクラムは戦術であり、リーン思考を実践するのは戦略となる。リーン思考を具体的にどう実践するかは自分たちで考える必要がある。あとは用語や概念の詳細な説明が主だった。リーン思考のメリットとして次があげられていた。

  • 早いタイミングでフィードバックを得られる
  • 早く作るとリスクを早期発見できる
  • 小さい状態でフィードバックをもらった方が手戻りが少なくなる

一方でリーン思考の概念はパラダイムシフトを伴うので難しいと説明されていた。難しい背景は次になる。

  • ウォータフォールはリソース効率を重視する考え方である
  • 産業革命の時代に科学的管理法としてウォータフォール的な考え方が広まった
    • 工場でのモノヅクリにはうまくいった
    • ソフトウェア開発ではあまり適用できない

また MVP(Minimum Viable Product)の意味を理解する。そして、なぜ私はEarliest Testable / Usable / Lovableを好むのか。 の考え方の例としてこの記事のスライドを引用していた。これはこれで理解できるけれど、これはプロダクトアウトの考え方であって、業務アプリケーションはまた違うのではないかと私には懸念に思えた。自分たちのプロジェクトにとってどう実践していくかが重要になるが、そういった話しはとくになかった。プロダクトオーナーからも現状とリーン思考の概念とのギャップや違和感を感じて質問していたようにみえた。

リーン思考は難しくてなかなか実践できないといった説明があったところに私は違和感を感じた。リーン思考とは無関係にそもそもプロジェクトマネジメントは難しい。現実のプロジェクトマネジメントは生きものなのでケーススタディや教科書通りの論理でできるものではないと私は考えている。現実のプロジェクトにおける、ヒト・モノ・カネ・情報の4つを考慮した上で意思決定を適切なタイミングで行うことに正解はない。歴史に if はないので重要な意思決定を下してそれがどんな結果になろうと、別の意思決定をしたときの未来を知ることはできない。うまくいかなかったときにできることはそのふりかえりだけで、リーン思考を実践できていないという考え方はやや責任転嫁しているようにも受け取れた。

k8s ノードの削除方法がわからない

1時に寝て7時に起きた。寝冷えしてお腹痛い。

eks クラスターの障害

日曜日にテスト環境の eks クラスターで障害が発生していた。k8s ノードが NotReady になっていて、しばらくすると NotReady,SchedulingDisabled に変わって、それから新しい k8s ノードが起動して古いものが削除されて置き換わった。おそらくエラーが発生し始めてから1時間ほどはかかっていたと思う。わりと時間がかかるので明らかに k8s ノードが不調だと人間が判断しているなら ec2 インスタンスの切り替えを早くやりたい。k8s の公式ドキュメントの Use kubectl drain to remove a node from service では次の手順で行うように書いてある。

$ kubectl drain <node name>

drain が正常終了すれば安全に k8s ノードを削除してよいのかな?

$ kubectl delete node <node name>

eks クラスターで障害が発生していたときに drain を実行するとエラーになったのでそのまま delete node したら k8s ノードは削除されたものの、自動的に新しい k8s ノードが起動しなかった。aws のマネジメントコンソールから ec2 インスタンスを調べたら起動したままだったので強制的に ec2 インスタンスを終了させたところ、オートスケールの設定から ec2 インスタンスが起動してきて復旧した。但し、このやり方は k8s が意図した手順ではないようにも思える。軽く調べた範囲では k8s ノードの正しい削除方法 (置き換え方法?) がみつからなかった。そんなことを日曜日に確認していたら月曜日にほぼ同じ現象が本番環境の eks クラスターでも発生した。私は一度経験していたので同僚に指示して経過を観察していた。ここで書いたのと同じような手順で復旧した。おそらく aws 側のなにかのメンテナンス作業でうちの eks クラスターだと k8s ノードが死んでしまうような作業があったのではないか?と疑いをもっている。

スクラムの悪いところをあげてみる

1時に寝て7時に起きて午前中はだらだらしていた。午後からオフィスでちょっと作業して夜はドラクエタクトしてた。

スクラムの悪いところ

金曜日に メタバース雑談会 に参加してチームビルディングや課題管理の雑談になった。そのときに課題管理やスクラムの概要を話してみたけど、多様なメンバーだといま一つ手応えがなくてコンテキストを揃えないと組織論の話しは難しいなと思えた。コンテキストが共有できていない状況ではあまり雑談に向く話題ではなかった。そんな中でスクラムの悪いところを考える機会が最近多いので簡単にまとめてみようと思う。もちろん、スクラムにはよいところもたくさんあって世の中に普及しているのはそれがためだと思う。よいところはすでにあちこちで言われているだろうから、あえて、ここでは悪いところを整理してみようと思う。

  • 心理的安全性のないチームではぬるま湯と化す
  • プロダクトオーナーに超人を要求する
  • スクラムイベントが時間とともに形骸化していく

スクラムが悪いのではなくチーム (組織) が悪いということもできるが、スクラムを1年近くやっていて陥っているのでスクラムの影響は大きいと考える。

心理的安全性のないチームではぬるま湯と化す

スクラムでは個人の責任を問われることはなく、名前の通り、チーム一丸となって課題に取り組むことを意図するプラクティスなので、悪く言えば、個人の怠慢が問われることもまずない。ここでいう怠慢は悪意をもったものではないとしても 社会的手抜き を容易に発生させる。個々のメンバーの責任を減少させると必然的に裁量も減少してしまう気がしている。ある課題がうまくいかなくても自分の責任とは言えない状況であればそのまま放置してチームが解決してくれるのを待つという選択が合理的になるケースが出てくる。これは人間の習性として抗いがたいと私は考えている。楽をしたいのが人間だと思う。必然的に楽な方に引き寄せられていく。克己心とまで言わなくても自分を律して課題に取り組んでいないとスクラムの枠組みではいくらでも楽をしてしまえる。業務としてその品質や納期に一定の厳しさをもっていないチームは簡単にぬるま湯と化してしまう。

プロダクトオーナーに超人を要求する

以前、こみやさんと話していたときに出た話題。スクラムはすべての責任はプロダクトオーナーが負うことになっている。プロダクトオーナーは開発者でないことが多いと思われる。建前としては開発がうまくいかないことの責任をすべてプロダクトオーナーが負うことになるが、これはフェアとは言い難い状況もある。はらさんと話していると、プロダクトオーナーは無茶苦茶な要求を開発者にしてしまいがちなのでそれをなだめたり指導したりするポジションとしてスクラムマスターがいるという話しも聞く。うちのプロダクトオーナーは人格者でまったく無茶な要求をしない。おそらく現場出身の方なのでシステムに疎いことは周りも理解していて開発責任を問われていないのではないか推測する。プロダクトオーナーに責任が集中している割にスクラムマスターが盾になることで開発への口出しは制限される。またスクラムマスターも業務上の責任をなんら負っていない。スクラムマスターの助言を遂行する責任はすべてプロダクトオーナーに求められる。スクラムは業務の役割分担としてプロダクトオーナー、スクラムマスター、開発リーダーの三位一体のような構成を取るものの、責任をプロダクトオーナーに押し付けつつ、プロダクトオーナーの権限を奪う構成ともみれる。

スクラムイベントが時間とともに形骸化していく

スクラムに不慣れなチームがスクラムガイドに書いてあることを実践していくには少し時間がかかるし、スクラムマスターのようなスクラムガイドを熟知しているメンバーに指導を仰ぐのは理に適っている。しかし、半年もやればスクラムガイドの手順やワークフローをメンバーは習熟する。実際にうちのスクラムマスターはふりかえりのファシリテーション以外にやることがないとちょくちょく発言していて、実際にデイリースクラムも半分ぐらい休んでいる。ファシリテーション以外で業務のイニシアティブを取ることはない。スクラムマスターがドメイン知識を身につけるべきかどうか、私はまだわからないが、うちのスクラムマスターはドメイン知識を習得していないので業務の話にはまったく入ってこれない。少し前に ゾンビスクラム という言葉を教えてもらったが、スクラムイベントのいくつかはただこなすだけの業務になりつつある。うちのチームのスプリントの実態やストーリーポイント運用は私からみたら課題だらけだが、ダメなところも含めてその予定調和に慣れてしまって複雑で難しい問題を改善しようとしていないように私からはみえている。

戦略シミュレーションと特性

ストレッチ

今日の開脚幅は開始前158cmで、ストレッチ後161cmだった。先週よりやや数字が悪くなった。それでも先週はストレッチを受けていても体全体のだるさのようなものを感じていたのが、今回はちゃんと筋を伸ばしている感覚があって先週の体調の悪さはなくなったように思える。右股関節周りの詰まりに加えて、右前ももの張りがいつもより大きかった気がする。毎週ストレッチを受けていると物理的な体調の良し悪しもわかるのがよい。

わざと負ける理由

一昨日からリアルタイム対戦 にはまっている。数をこなしていてわかってきたことがたくさんある。戦略シミュレーションゲームはやればやるほど学びがある。

  • パーティーの特性によって相性のよい対戦相手とそうじゃないのがある
    • リアルタイム対戦のマッチングは選択できないのでマッチングしないと相手がわからない
    • 相性の悪い相手だとマッチング時点で辞退する (負けを認める) 相手もいる
  • こっちの戦略に呼び込むための布石がいる
    • 3つぐらい戦略を用意して最終的にどの戦略に呼び込むかを相手の動きをみながら考えないといけない
    • キャラを動かす制限時間が15秒なのでわりと忙しい
    • こちらの布石にはめて最終的に勝てると達成感がある
      • 中盤まで負けていて向こうに勝ったと思わせて逆転できるとなお嬉しい
      • ダークドレアム は奇数ターン (1, 3, 5, 7 …) ごとに攻撃力・守備力・すばやさ・かしこさが1段階上がるバフがかかる
        • ダークドレアムはすばやさが低いので徐々にすばやさが上がっていって先制できるようになると最後の1手違いで勝つ状況が出来上がる
        • ダークドレアムで5ターンまで戦闘を継続できれば勝率が高い
  • すばやさの高いパーティーには何もできないから勝てない
    • 高すばやさ (且つ、高火力または状態異常) パーティーには絶対に勝てない
      • リアルタイム対戦でダークドレアムがあまり使われていない理由だと思う
    • 初手前にパーティーの半分ぐらいがやられている
    • 相手の攻撃が届かない初期配置がないので絶対に防げない
  • 状態異常攻撃の主体パーティーは対戦していておもしろくない
    • 混乱・麻痺・眠り・魅了といった状態異常になると2ターン何もできない
    • 状態異常攻撃 + 高すばやさのキャラの攻撃を防ぐ方法はない
  • パーティー編成のウェイト制限がうまく調整されている
    • 高ランク (ステータス高い) のキャラばかりを編成できないようにウェイト制限がある
    • これにより、パーティー編成が4人か5人かにわかれる
    • 低ランク (ステータス低い) のキャラはウェイトも低いので5人目には入れやすい

本題のわざと負ける理由だが、わかってきた。リアルタイム対戦のマッチングは同じランク内で行われる。上位のランクになればなるほど、対戦相手も強くなる (強くなければ上位のランクに上がれない) 。一定量のポイントを獲得するとランクアップしていけるが、負けるとポイントが下がるペナルティがつく。一定のランクで勝ち負けを繰り返すとそのランクに留まり続けるということができる。私も自分の限界までランクアップしてみて気付いたのは周りも強いので限界に到達すると勝ったり負けたりを繰り返す。ここで別のルールで勝った回数に応じてコインが支払われるボーナスがある。強い人が低いランクで勝ち数を稼ぐのは限界に近いランクで勝ったり負けたりを繰り返すよりもはるかに効率がよい。それはリアルタイム対戦のマッチングに時間制限があるからなおさらそうなる。実力が均衡した相手と時間をたくさん使って2勝3敗を繰り返すよりも、わざと負けながら弱い相手に勝ち続ける5勝?敗を繰り返す方が絶対数としての勝ち数を稼ぐには効率がよい。おそらく負けたときのペナルティがあるランクからわざと負ける人が現れ始めていると思う。なるほどなぁ。

dto に対するリフレクションの是非

1時に寝て7時に起きた。

ドラクエタクトのリアルタイム対戦

もう2年間もずっとゲームし続けている。最近 リアルタイム対戦 モードがリリースされた。全然やる気なかったんやけど、リアルタイム対戦で得たコインでもらえるアイテムが魅力的なのでやることにした。そして、実際にアルタイム対戦をやってみるとはまる。運営の手の平でゲームさせられている。

  • 人間が相手で狡猾な戦略で負けると悔しい
  • 人間が相手の戦略の方が創意工夫があって学びになる
  • 実際にやり始めるとおもしろくなってきてずっとやってしまう

できる時間を制限しているというのもうまいやり方だなと思っている。朝・昼・晩の7-9時、12-14時、19-22時に制限している。そこまでしてリアルタイムに人間同士をマッチングしてゲームさせる必要があるのか?という素朴な疑問に私は辿りつくが、おそらくゲーム開発者からみたらそうじゃない大事なユーザー体験があるのだろうと推測する。あと不思議なことが1つ。マッチングしていると、たまにわざと負けてくれる人がマッチングされる。チームのメンバーが1人で向こうが先行なら戦いを辞退 (こちらの勝ちになる) するし、こちらが先行でもすぐにやっつけられる。ちゃんと統計をとってないけど、20回に1回ぐらいの頻度でわざと負けてくれる人とマッチングする。あの人たちは一体どういう理由でわざと負けているんだろう?

リフレクションのユーティリティを作った

いまお手伝いで開発している api サーバーは外界と内部のデータの境界を明確にわけていて、外向けのオブジェクト定義と内部向けのオブジェクト定義が異なる。ほとんど同じデータであっても dto を介して値を受け渡ししないといけない。そうすると、次のような dto と他のオブジェクトとの値渡しのための処理が型ごとにあちこちに実装されている。

private MyRecord toMyRecord(MyDataInput in) {
    var record = new MyRecord();
    record.id = in.id
    record.name = in.name;
    record.someId1 = in.someId1;
    record.someId2 = in.someId2;
    record.someId3 = in.someId3;
    record.sortOrder = in.sortOrder;
    record.createUser = in.createUser;
    record.updateUser = in.updateUser;
    ...
}

メンバー数が20-30ぐらいあると、たまに値のセット忘れがあったり、あとから追加したメンバーの保守ができてないとか、たまにトラブルが起きる。これ自体は間違っているわけじゃなくて境界を明確にわけるメリットもあるのでプログラミングの煩雑さとトレードオフと言える。

最近、私が管理系の web api のエンドポイントを作る機会が多いせいか、dto と外部向けのオブジェクトを明確にわける必要のない要件もあったりする。試しにリフレクションを使って同名のフィールド間の値の受け渡しは自動でやってみたらどんな感じかな?と思って作ってみた。

public class ReflectionUtil {

    private ReflectionUtil() {
        throw new AssertionError("ReflectionUtil is a utility class");
    }

    private static <T> Field getField(Class<T> klass, String fieldName) {
        try {
            return klass.getDeclaredField(fieldName);
        } catch (NoSuchFieldException e) {
            return null;
        }
    }

    public static <T1, T2> T2 mapFieldValues(T1 fromInstance, T2 toInstance) {
        var fromClass = fromInstance.getClass();
        for (var toField : toInstance.getClass().getDeclaredFields()) {
            toField.setAccessible(true);
            var fromField = getField(fromClass, toField.getName());
            if (fromField != null) {
                fromField.setAccessible(true);
                try {
                    toField.set(toInstance, fromField.get(fromInstance));
                } catch (IllegalAccessException e) {
                    throw new RuntimeException(e);
                }
            }
        }
        return toInstance;
    }
}

これを使うと、先のコードがこれだけで済む。煩わしい値の受け渡しだけのコードを削減できる。

var record = ReflectionUtil.mapFieldValues(in, new MyRecord());

こんなことやると、セキュリティ的によくないとか反論されるかな?と思いながら pr を出してみたら思いの外、好評だったのでちょっと使ってみようと思う。

次のお仕事探し

22時に寝て5時に起きた。前日はあまり寝てなかったら眠くなった。午前中に昨日やり残した開発のお仕事を4時間ほどやってから自分の会社の雑務をしていた。

智将・諸葛孔明の兵法

智将・諸葛孔明の兵法の書評 をみかけた。amazon.co.jp に本の情報があることは確認できたが、1987年に出版された本なので在庫はないから諦めていた。たまたま先日メルカリで売っているのをみつけたので購入した (300円) 。第一部生涯 7.馬謖を斬るのところで第一次北伐の背景や概要が説明されている。馬謖が山の上に陣取ったのは短期決戦を意図してその優位性を取ろうとしたのだろうと著者の推察が述べられている。また馬謖を斬るにいたったのは軍律を守ることをすべてに優先したと説明されている。みずからが軍律を破ってしまうと蜀全体の維持が不可能になるだろうと著者も推察が述べられている。そのときに涙したことも触れているが、涙した理由の詳細については言及していない。

リモートワーク前提のお仕事

以前、登録した人材紹介プラットフォームremogu さんがある。待遇のよい案件はほぼ東京か、あっても大阪になる。私は神戸に住んでいるので基本的にリモートワークでないとお仕事を探すのは難しい。remogu さんはリモートワーク前提なのでフィルター条件が1つ減るので検索しやすい。11月から新しいお仕事を探さないといけない。remogu さんのプロフィール情報を求職のステータスに更新しておいた。すると翌日に過去に面談したエージェントさんから連絡があって、すぐに希望に沿った案件を4つほど提案してくれた。その業務内容を今日みていたらどれも私の希望した業務内容に合致するものだったので優先順位とともに職務経歴書を更新して返信した。以前、面談したときもこのエージェントさんの印象はよかった。なにかしら相性があるのか、よい印象をもつエージェントさんだとよい提案をしてくれる。この前、初めて登録した人材紹介会社のエージェントさんとの面談はいまひとつだったのでなにかしらエージェントさんの質なのか、人間力の差による違いがあるんだなと思う。順番に面談していってマネジメントのキャリアを得られるかどうか、私にとっては大きな挑戦の1つになるので求職活動をがんばりたい。

アプリケーションとジョブの違いと一時停止

2時に寝て5時に起きた。たった3時間しか寝てないのに夜中に1回は起きている。スポットで9時から会議が入ったのでそれまでに朝のお仕事を終わらせようと思ったら早起きできた。6時前にはオフィスに行ってお仕事を始められた。

アプリケーションからジョブへの移行

k8s の cronjob を活用する前は spring の Scheduled アノテーションで定期実行処理を書いていた。アプリケーションサーバー内の1スレッドが定期実行していた。これはスケールアウト前提のアプリケーションサーバーには向いてなくて、複数のアプリケーションサーバーをスケールアウトさせてデプロイすると、定期処理も複数実行されてしまい、同時実行できない類の処理だと問題になる。k8s の cronjob は同時実行しない設定などもあるため、k8s の cronjob へ移行している。

移行作業の準備をしていてアプリケーションサーバーの pod は replicas を調整することで一時停止の代替となるオペレーションができる。

$ kubectl scale --replicas=0 deployment/my-app
deployment.apps/my-app scaled

移行時のアプリケーションサーバーはこれで無効にしておき、cronjob に入れ替えるといった切り戻し可能な状態で移行できる。cronjob も suspend のフラグを使うことで一時停止できるので検証しながら双方を切り替えることができる。

$ kubectl patch cronjob my-batch-job1 -p '{"spec": {"suspend": true}}}'
cronjob.batch/my-batch-job1 patched