RubyもJavaも楽しく学ぶ

Rubyist近況 Advent Calendar 2021 11日目です。

今北産業

  • 画伯を自称して3年以上絵を描き続けています
  • いわゆるSIerでドキュメントを書いたり、コードを書いたり色々なことをやっています
  • Ruby関連の取り組みも色々とやっています

画伯

ふとしたきっかけで付箋紙に絵を描いて、 #miyohide_gahaku のハッシュタグ付きでTwitterにアップしています。気がつけば3年以上描き続けています。同じテーマで書いた絵をみると画力がどれだけあがったかわかる感じです。一番最初に書いたのが左上のもので、そこから右上→左下→右下という流れ。

f:id:miyohide:20211209205316j:plain

基本は下書きなしの一発勝負。何も見ずに描くほどの力はないので、適当にお手本を検索してそれを元に描いています。

ランニング

健康維持のためにはじめたランニングも10年ほど経ちました。今年はちょっと体調を崩した時期があったのですが、そこそこの距離を走れている結果となりました。目標としている年間2,000kmを今年は達成しました。

f:id:miyohide:20211210211921p:plain

調子に乗ると怪我するので、安全に。

私自身はもともと運動ができる人ではないので、速く走るということはせず、ゆっくり長く走るようにしています。

仕事

いろんなことがあって、最近はJavaのコードを書くことが多いです。Spring何も分からない…

いろんな言語やフレームワークを学ぶことは大事だと思っていて、特定の言語・フレームワーク以外はやりませんということはしないようにしています。JavaからRubyへ、RubyからJavaへ。それぞれの言語やフレームワークの特徴が楽しめるようになりました。

『達人プログラマー』の中で「毎年、新たなプログライング言語を1つは学ぶこと」と書かれていることは有名で、それをちょっとだけ意識しています。

Ruby

Rubyでもいろいろとコードを書いています。ちょっと変わったところでは、少し前にAzure Functions上でRubyのコードを動かすということをやりました。

miyohide.hatenablog.com

仕事上Azureを使うことが多いので、そのSDKであるazure-sdk-for-rubyAzure storage Client Library for Rubyのコードを眺めてできることならPull Requestを出したりしています。

るびま」ことRubyist Magazineについては、今年は発行できない感じになりそうです。いろいろとやりたいことはあって以下のようなことをしたいと思っています(順不同)。

  • デザインの改善
  • デプロイの流れをGitHub Actionsを使って実装するようにする
  • 誤字脱字修正
  • CSS改善
  • 古い記事の更新
    • 例えば、RSpecの記事は結構見られているだけれども、バージョンがかなり古いので新しいバージョンに対応したい
  • より頻繁に記事が出せるように形式を変える
    • 複数記事をまとめて、「号」という単位で出すのはちょっとしんどいかなと

書き出したらいっぱいあった...あまり手がついていないので、できるところから徐々にやっていこうかなと思っています。

Yokohamarb

主に私が開催していたYokohama.rbは、年に数回オンラインで雑談する会になっています。それはそれでいいかなと思う一方で何かテーマを決めてコードを書きあうみたいなことをやりたいとも思っています。いずれも行動に移せていないので来年こそはと思っている今日この頃です。

明日

明日の近況 Rubyist はしおいさんです。よろしくお願いします。

Azure Cache for RedisのベストプラクティスをSpring Bootで検証する

はじめに

Azure Static Web Appsの検証が思いのほかサクサクできてしまったので、新しいテーマ。今回からはAzure Cache for Redisのベストプラクティスを検証していきたいと思います。

Azure Cache for Redisのドキュメントを見ていると、「接続の回復力に関するベストプラクティス」というドキュメントを見つけました。

docs.microsoft.com

以前、Spring BootアプリからAzure Cache for Redisへの接続をするためのブログ記事を載せたのですが、それをもう少し発展させてより有益な情報としていきたいと思います。

miyohide.hatenablog.com

サンプルアプリの挙動

今回、Azure Cache for Redisへの接続にはSpring Session Data Redisを使います。

spring.io

アプリケーションの挙動は以下の通りです。

  1. /にGETでアクセスすると、セッションの情報(SessionInfoの値)を確認します
  2. 値があればそのデータをもとにGreetingオブジェクトを作成して返します
  3. 値が無ければセッションにIdやnameなどの値を保存し、Greetingオブジェクトを作成して返します

実装コードとしては以下の通り。

    @GetMapping("/")
    public Greeting greeting(@RequestParam(value = "name", defaultValue = "World") String name) {
        logger.info("GET to '/'. name = [" + name + "], sessionInfo = [" + sessionInfo.toString() + "]");
        if (sessionInfo.getName() == null) {
            sessionInfo.setId(counter.incrementAndGet());
            sessionInfo.setName(name);
            sessionInfo.setCreatedAt(new Date());
        }
        return new Greeting(sessionInfo.getId(), String.format(template, sessionInfo.getName()),
                sessionInfo.getCreatedAt());
    }

ブラウザ画面には次のように表示されます。

f:id:miyohide:20211205154048p:plain

アプリケーションのバージョンアップ

上記ブログで使っていたSpring Bootは2.3.1.RELEASEなので、まずはこれをバージョンアップします。今回使ったバージョンは2.5.7です。

前回検証した時は、SessionConfigクラスを作成する必要がありました。今回、Spring Bootを2.5.7に上げてSessionConfigクラスを消してAzure Cache for Redisに接続してみましたが、以下の例外が出力され起動に失敗しました。

Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled.
2021-12-05 14:59:11.427 ERROR 13036 --- [           main] o.s.boot.SpringApplication               : Application run failed

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'enableRedisKeyspaceNotificationsInitializer' defined in class path resource [org/springframework/boot/autoconfigure/session/RedisSessionConfiguration$SpringBootRedisHttpSessionConfiguration.class]: Invocation of init method failed; nested exception is org.springframework.data.redis.RedisConnectionFailureException: Unable to connect to Redis; nested exception is io.lettuce.core.RedisConnectionException: Unable to connect to hogehoge.redis.cache.windows.net:6380
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1804) ~[spring-beans-5.3.13.jar:5.3.13]
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:620) ~[spring-beans-5.3.13.jar:5.3.13]
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542) ~[spring-beans-5.3.13.jar:5.3.13]
        at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335) ~[spring-beans-5.3.13.jar:5.3.13]
        at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) ~[spring-beans-5.3.13.jar:5.3.13]
        at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333) ~[spring-beans-5.3.13.jar:5.3.13]
        at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208) ~[spring-beans-5.3.13.jar:5.3.13]
        at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:944) ~[spring-beans-5.3.13.jar:5.3.13]
        at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:918) ~[spring-context-5.3.13.jar:5.3.13]
        at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:583) ~[spring-context-5.3.13.jar:5.3.13]
        at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:145) ~[spring-boot-2.5.7.jar:2.5.7]
        at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:765) ~[spring-boot-2.5.7.jar:2.5.7]
        at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:445) ~[spring-boot-2.5.7.jar:2.5.7]
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:338) ~[spring-boot-2.5.7.jar:2.5.7]
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:1354) ~[spring-boot-2.5.7.jar:2.5.7]
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:1343) ~[spring-boot-2.5.7.jar:2.5.7]
        at com.example.demo.DemoApplication.main(DemoApplication.java:10) ~[main/:na]
Caused by: org.springframework.data.redis.RedisConnectionFailureException: Unable to connect to Redis; nested exception is io.lettuce.core.RedisConnectionException: Unable to connect to hogehoge.redis.cache.windows.net:6380
        at org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory$ExceptionTranslatingConnectionProvider.translateException(LettuceConnectionFactory.java:1689) ~[spring-data-redis-2.5.7.jar:2.5.7]
        at org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory$ExceptionTranslatingConnectionProvider.getConnection(LettuceConnectionFactory.java:1597) ~[spring-data-redis-2.5.7.jar:2.5.7]
        at org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory$SharedConnection.getNativeConnection(LettuceConnectionFactory.java:1383) ~[spring-data-redis-2.5.7.jar:2.5.7]
        at org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory$SharedConnection.getConnection(LettuceConnectionFactory.java:1366) ~[spring-data-redis-2.5.7.jar:2.5.7]
        at org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory.getSharedConnection(LettuceConnectionFactory.java:1093) ~[spring-data-redis-2.5.7.jar:2.5.7]
        at org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory.getConnection(LettuceConnectionFactory.java:421) ~[spring-data-redis-2.5.7.jar:2.5.7]
        at org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration$EnableRedisKeyspaceNotificationsInitializer.afterPropertiesSet(RedisHttpSessionConfiguration.java:331) ~[spring-session-data-redis-2.5.3.jar:2.5.3]
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1863) ~[spring-beans-5.3.13.jar:5.3.13]
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1800) ~[spring-beans-5.3.13.jar:5.3.13]
        ... 16 common frames omitted
Caused by: io.lettuce.core.RedisConnectionException: Unable to connect to hogehoge.redis.cache.windows.net:6380
        at io.lettuce.core.RedisConnectionException.create(RedisConnectionException.java:78) ~[lettuce-core-6.1.5.RELEASE.jar:6.1.5.RELEASE]
        at io.lettuce.core.RedisConnectionException.create(RedisConnectionException.java:56) ~[lettuce-core-6.1.5.RELEASE.jar:6.1.5.RELEASE]
        at io.lettuce.core.AbstractRedisClient.getConnection(AbstractRedisClient.java:330) ~[lettuce-core-6.1.5.RELEASE.jar:6.1.5.RELEASE]
        at io.lettuce.core.RedisClient.connect(RedisClient.java:216) ~[lettuce-core-6.1.5.RELEASE.jar:6.1.5.RELEASE]
        at org.springframework.data.redis.connection.lettuce.StandaloneConnectionProvider.lambda$getConnection$1(StandaloneConnectionProvider.java:115) ~[spring-data-redis-2.5.7.jar:2.5.7]
        at java.base/java.util.Optional.orElseGet(Optional.java:369) ~[na:na]
        at org.springframework.data.redis.connection.lettuce.StandaloneConnectionProvider.getConnection(StandaloneConnectionProvider.java:115) ~[spring-data-redis-2.5.7.jar:2.5.7]
        at org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory$ExceptionTranslatingConnectionProvider.getConnection(LettuceConnectionFactory.java:1595) ~[spring-data-redis-2.5.7.jar:2.5.7]
        ... 23 common frames omitted
Caused by: java.io.IOException: Connection reset by peer
        at java.base/sun.nio.ch.FileDispatcherImpl.read0(Native Method) ~[na:na]
        at java.base/sun.nio.ch.SocketDispatcher.read(SocketDispatcher.java:39) ~[na:na]
        at java.base/sun.nio.ch.IOUtil.readIntoNativeBuffer(IOUtil.java:276) ~[na:na]
        at java.base/sun.nio.ch.IOUtil.read(IOUtil.java:233) ~[na:na]
        at java.base/sun.nio.ch.IOUtil.read(IOUtil.java:223) ~[na:na]
        at java.base/sun.nio.ch.SocketChannelImpl.read(SocketChannelImpl.java:356) ~[na:na]
        at io.netty.buffer.PooledByteBuf.setBytes(PooledByteBuf.java:253) ~[netty-buffer-4.1.70.Final.jar:4.1.70.Final]
        at io.netty.buffer.AbstractByteBuf.writeBytes(AbstractByteBuf.java:1132) ~[netty-buffer-4.1.70.Final.jar:4.1.70.Final]
        at io.netty.channel.socket.nio.NioSocketChannel.doReadBytes(NioSocketChannel.java:350) ~[netty-transport-4.1.70.Final.jar:4.1.70.Final]
        at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:151) ~[netty-transport-4.1.70.Final.jar:4.1.70.Final]
        at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:719) ~[netty-transport-4.1.70.Final.jar:4.1.70.Final]
        at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:655) ~[netty-transport-4.1.70.Final.jar:4.1.70.Final]
        at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:581) ~[netty-transport-4.1.70.Final.jar:4.1.70.Final]
        at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:493) ~[netty-transport-4.1.70.Final.jar:4.1.70.Final]
        at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:986) ~[netty-common-4.1.70.Final.jar:4.1.70.Final]
        at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74) ~[netty-common-4.1.70.Final.jar:4.1.70.Final]
        at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30) ~[netty-common-4.1.70.Final.jar:4.1.70.Final]
        at java.base/java.lang.Thread.run(Thread.java:829) ~[na:na]

そのため、まだapplication.propertiesにてspring.redis.ssl=trueを設定し、以下のようなSessionConfigクラスを作ってあげる必要があります。(前回掲載のコードから少しリファクタリング)。

package com.example.demo;

// import文は省略

@Configuration
@EnableRedisHttpSession
public class SessionConfig extends AbstractHttpSessionApplicationInitializer {
  @Bean
  public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
      return new GenericJackson2JsonRedisSerializer();
  }
  @Bean
  public static ConfigureRedisAction configureRedisAction() {
      return ConfigureRedisAction.NO_OP;
  }
}

注)当初、connectionFactoryメソッドも実装していましたが、spring.redis.ssl=trueapplication.propertiesに設定しておくだけでOKでした。

再起動を実施する

マイクロソフトのドキュメントには、

再起動を使用して修正プログラムをシミュレートし、接続の中断に対するシステムの回復性をテストします。

とあるので、再起動を実施します。

再起動はAzure Portal上から簡単に実施できます。Standard以上ですと複数台のマシンでAzure Cache for Redisが動いているので、再起動対象を一つ選んで「再起動」をクリックするだけです。ここでは「プライマリ」を選択して再起動してみます。

f:id:miyohide:20211205151058p:plain

Azure Portalの画面に表示されますが、「再起動」をクリックしてもすぐに再起動が行われるわけではなく、最大で2分ほど待たされるようです。

f:id:miyohide:20211205151332j:plain

アプリの挙動

再起動を実施してから定期的にブラウザをリロードしてみてセッションの情報を参照するようにしてみます。

ログを見ると再起動して少し待ったタイミングで「Reconnectiong」とか「Reconnected to」という文言が見え再起動による再接続が行われていることがわかります。

2021-12-05 15:37:23.316  INFO 13111 --- [nio-8080-exec-9] com.example.demo.GreetingController      : GET to '/'. name = [World], sessionInfo = [SessionInfo id=[1], name=[ThisIsATest], createdAt=[Sun Dec 05 15:23:32 JST 2021]]
2021-12-05 15:37:23.658  INFO 13111 --- [xecutorLoop-1-1] i.l.core.protocol.ConnectionWatchdog     : Reconnecting, last destination was hogehoge.redis.cache.windows.net/52.253.104.134:6380
2021-12-05 15:37:23.658  INFO 13111 --- [xecutorLoop-1-8] i.l.core.protocol.ConnectionWatchdog     : Reconnecting, last destination was hogehoge.redis.cache.windows.net/52.253.104.134:6380
2021-12-05 15:37:24.082  INFO 13111 --- [ioEventLoop-6-5] i.l.core.protocol.ReconnectionHandler    : Reconnected to hogehoge.redis.cache.windows.net:6380
2021-12-05 15:37:24.094  INFO 13111 --- [io-8080-exec-10] com.example.demo.GreetingController      : GET to '/'. name = [World], sessionInfo = [SessionInfo id=[1], name=[ThisIsATest], createdAt=[Sun Dec 05 15:23:32 JST 2021]]
2021-12-05 15:37:24.362  INFO 13111 --- [ioEventLoop-6-6] i.l.core.protocol.ReconnectionHandler    : Reconnected to hogehoge.redis.cache.windows.net:6380
2021-12-05 15:37:26.834  INFO 13111 --- [nio-8080-exec-1] com.example.demo.GreetingController      : GET to '/'. name = [World], sessionInfo = [SessionInfo id=[1], name=[ThisIsATest], createdAt=[Sun Dec 05 15:23:32 JST 2021]]

また、再接続後もセッションに入っているデータは(今回は)失われていないようでした。ただ、再起動の画面には「データが失われる可能性があります」ということなので、今回はたまたまだったのかもしれません。

なお、今回は再起動時に「プライマリ」を選択しました。今回のように「Reconnectiong」とか「Reconnected to」は「プライマリ」を選択した時に起きる挙動で「レプリカ」を再起動の対象としてもとくに再接続をしているような挙動は見受けられませんでした。

今回のまとめ

非常に単純なサンプルですが、Spring Boot 2.5.7にて作ったAzure Cache for Redisを使ったプログラムでは、特段再接続の処理を自分で作り込まなくても再接続はやってくれそうですね。

今後は他の推奨事項について検証してみたいと思います。

ソース

ここまでの検証ソースはこちら。

github.com

Azure Static Web Appsを使ってみる(2)Reactアプリ+Azure Functionsの構成でアプリを動かす

はじめに

先日からAzure Static Web Appsを使ってみています。単なるHTMLだけではなく、ReactやVueなどで作られたページもホスティングでき、APIとしてAzure Functionsも動かすことができるというものということで、実際に試してみています。

前回はReactのチュートリアルであった三目並べを動かしてみました。

miyohide.hatenablog.com

今回はAzure Functionsとの連携を試してみたいと思います。

Azure Static Web AppsでのAzure Functions

Azure Static Web AppsでのAzure Functionsは、以下のドキュメントが示すように二種類の利用方法があります。

  • マネージド関数
  • 独自の関数の持ち込み

docs.microsoft.com

色々と自由度が高いのは「独自の関数の持ち込み」ですが、今回はお手軽に試すために「マネージド関数」を使って実装したいと思います。

なお2021年11月の時点で、マネージド関数のデプロイ先は以下の画像のように「Central US」「East US 2」「East Asia」「west Europe」「West US 2」の5つに限られていました。時が解決するとは思いますが、Japan Eastへのデプロイが必要な場合は「独自の関数の持ち込み」を利用する必要がありそうです。

f:id:miyohide:20211128114651p:plain

マネージド関数を使ってAPIを実装する

マネージド関数を使ってAPIを実装します。以下のドキュメントにチュートリアルが書かれていますので、これを参考にして作っていきます。

docs.microsoft.com

以下のように、JSONデータを返す簡単なAPIにしました。

module.exports = async function (context, req) {
    context.log('JavaScript HTTP trigger function processed a request.');
    context.res = {
        headers: {
            "Content-Type": "application/json"
        },
        body: [
            {id: 1, title: "title1", body: "body1"},
            {id: 2, title: "title2", body: "body2"},
            {id: 3, title: "title3", body: "body3"},
        ]
    }
};

Reactアプリを作る

APIを呼び出すReactアプリを作ります。以下のサイトを参考に実装していきました。

qiita.com

CSSは私が慣れているReact Bootstrapを利用しました。

react-bootstrap.github.io

APIの呼び出し部分の実装は以下の通りです。API/api/postとしているので、このAPIaxiosを使って呼び出します。

import React, {useEffect, useState} from 'react';
import Container from 'react-bootstrap/Container';
import Row from 'react-bootstrap/Row';
import Col from 'react-bootstrap/Col';
import BodyCard from './BodyCard';
import axios from 'axios';

function Content() {
  // Reactのステートフックを利用してstate変数であるpostを初期化し、
  // 更新するsetPosts関数を取得する。
  // ここでは、postを[]で初期化する
  const [post, setPosts] = useState([])

  // useEffectを利用してJSONデータを取得してsetPostsでstate変数である
  // postを更新する
  useEffect(() => {
    axios.get('/api/post')
    .then(res => {
      setPosts(res.data)
    })
  }, [])

  // 名前付き関数(アロー関数式を使った記述)
  const getCardContent = getObj => {
    return (
      <Col key={getObj.id}>
        {/* スプレッド構文。getObjのすべての要素をBodyCardのproperty(props)に渡す */}
        <BodyCard {...getObj} />
      </Col>
    );
  };
  return (
    <Container>
      <Row xs={1} md={3} className="g-2">
        {/* map() は与えられた関数を配列の全ての要素に対して呼び出し、
            その結果からなる新しい配列を生成する
        */}
        {post.map(contentObj => getCardContent(contentObj))}
      </Row>
    </Container>
  );
}

export default Content;

データ表示を行なっているBodyCardは以下のような実装です。

import React from 'react';
import Card from 'react-bootstrap/Card';

function BodyCard(props) {
  // 分割代入。オブジェクト(props)の各プロパティに対応した
  // 変数に代入する。
  // see. https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment
  const { title, body } = props;

  return (
    <Card>
      <Card.Body>
        <Card.Title>{title}</Card.Title>
        <Card.Text>
          {body}
        </Card.Text>
      </Card.Body>
    </Card>
  );
}

export default BodyCard;

全体実装は、末尾に載せているGitHubリポジトリを参照してください。

デプロイ

Azure Static Web AppsへのデプロイはGitHub ActionsかAzure DevOpsを利用する必要があります。前回はWebアプリだけをデプロイしましたが、そこからの差分はapi_locationを追加しただけです。以下のような実装になりました。

name: Azure Static Web Apps CI/CD

on:
  push:
    branches:
      - main

jobs:
  build_and_deploy_job:
    runs-on: ubuntu-latest
    name: Build and Deploy Job
    steps:
      - uses: actions/checkout@v2
      - name: Build and Deploy
        id: builddeploy
        uses: Azure/static-web-apps-deploy@v1
        with:
          azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN }}
          repo_token: ${{ secrets.GITHUB_TOKEN }}
          action: "upload"
          app_location: "posts"
          api_location: "api"
          output_location: "build"

動作確認

デプロイした後にAzure Static Web AppsのURLにアクセスすると無事動いていることが確認できました。

f:id:miyohide:20211128114452p:plain

Azure Portal上でAzure Static Web Appsの「設定」-「関数」をみると、今回デプロイした「post」が表示されていました。ただ、これは表示されているだけで、クリックしても何も動作しませんでした。

f:id:miyohide:20211128114832p:plain

現在のソース

github.com

Azure Static Web Appsを使ってみる

はじめに

Azure Static Web Appsはその名前が示すとおり、静的Webアプリと呼ばれるWebアプリを公開するためのサービスです。単なるHTMLだけではなく、ReactやVueなどで作られたページもホスティングでき、APIとしてAzure Functionsも動かすことができるというものです。 これから数回はAzure Static Web Apps上で静的なWebアプリとAPIアプリを動かすことにします。

Reactでアプリを作る

Azure Static Web AppsにデプロイするアプリとしてReactのチュートリアルで書かれていた三目並べを実装してみます。

ja.reactjs.org

最終的にはこんなアプリとなります。

f:id:miyohide:20211121163248p:plain

Azure Static Web Appsにデプロイする

Azure Static Web Appsにデプロイする方法としては、GitHub ActionsかAzure DevOpsを使います。ここではGitHub Actionsを使うことにします。

Azure Portal上でAzure Static Web Appsを作る際は「デプロイの詳細」でデプロイ方法を指定します。

f:id:miyohide:20211121164750p:plain

ここでGitHubを選ぶとGitHub Actionsが自動生成され、デプロイも行われます。

f:id:miyohide:20211121164929p:plain

詳細はチュートリアルを参照してください。

docs.microsoft.com

ここではこの便利な機能を使わず、自分で実装してみることにします。

自分でデプロイする処理を書いてみる

今回は、ひとつのリポジトリの中に複数のプロジェクトを格納する形をとっています。具体的には次のような形。

.
 |-posts
 | |-node_modules
 | |-public
 | |-src
 |-my-app  ← 今回はこのディレクトリ以下のものをデプロイしたい
 | |-node_modules
 | |-public
 | |-build
 | |-src
 |-.github
 | |-workflows

今回は「my-app」以下のものをデプロイしてみることにします。

GitHub ActionsではAzure/static-web-apps-deployを使って以下のように実装します。

name: Azure Static Web Apps CI/CD

on:
  push:
    branches:
      - main

jobs:
  build_and_deploy_job:
    runs-on: ubuntu-latest
    name: Build and Deploy Job
    steps:
      - uses: actions/checkout@v2
      - name: Build and Deploy
        id: builddeploy
        uses: Azure/static-web-apps-deploy@v1
        with:
          azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN }}
          repo_token: ${{ secrets.GITHUB_TOKEN }}
          action: "upload"
          app_location: "my-app"
          output_location: "build"

app_locationにデプロイするアプリがあるパスを、output_locationにビルド結果の格納パスを指定すればよかったです。詳細は以下のマニュアルを参照すると良いかと思います。

docs.microsoft.com

AZURE_STATIC_WEB_APPS_API_TOKENの取得

AZURE_STATIC_WEB_APPS_API_TOKENはAzure PortalのAzure Static Web Appsの概要ページにある「デプロイトークンの管理」をクリックしたときに表示されるデプロイトークンをGitHubの該当リポジトリにあるシークレットに登録します。

f:id:miyohide:20211121173725p:plain

GitHubでの登録方法については以下を参照してください。

docs.github.com

app_locationに誤ったパスを指定した場合

ちなみに、app_locationmy-appではなくmy-app/srcを指定した場合、Error: Could not detect the language from repo.が発生してうまくビルドできませんでした。該当のログは以下の通り。

---Oryx build logs---


Operation performed by Microsoft Oryx, https://github.com/Microsoft/Oryx
You can report issues at https://github.com/Microsoft/Oryx/issues

Oryx Version: 0.2.20211001.1, Commit: f0cbc9b1f0d056493cdb36f92b62f11921c87261, ReleaseTagName: 20211001.1

Build Operation ID: |EvAudJVgQ3c=.cd954f65_
Repository Commit : 58887517aac8ce2f34a54cd9a104ace070e0e382

Detecting platforms...
Could not detect any platform in the source directory.
Error: Could not detect the language from repo.


---End of Oryx build logs---
Oryx was unable to determine the build steps. Continuing assuming the assets in this folder are already built. If this is an unexpected behavior please contact support.
Finished building app with Oryx
Failed to find a default file in the app artifacts folder (/). Valid default files: index.html,Index.html.
If your application contains purely static content, please verify that the variable 'app_location' in your workflow file points to the root of your application.
If your application requires build steps, please validate that a default file exists in the build output directory.

output_locationに誤ったパスを指定した場合

ちなみに、output_locationbuildではなくmy-app/buildを指定した場合、The app build failed to produce artifact folder: 'my-app/build'.と出てデプロイできませんでした。

結果

これで無事デプロイできたのでAzure Static Web AppsのURLにアクセスすると、無事動きました。

f:id:miyohide:20211121173853j:plain

ソース

本記事時点でのソースは以下の通りです。

github.com

RubyスクリプトをDockerイメージ化する

はじめに

ActiveRecordの簡単な検証をしようと思っていたのですが、色々と環境を作るのが面倒臭いなと思いDockerでやってみました。いくつかハマりポイントがあったので、備忘録的に記しておきます。

参考

以下のQiitaの記事を参考にしました。この記事ではMySQLで記されていますが、私の趣味としてPostgreSQLにて実装しています。

qiita.com

Dockerで環境を作る

Ruby環境とPostgreSQL環境をDockerで用意するため、以下のdocker-compose.ymlファイルを用意します。

version: "3.8"
services:
  db:
    image: postgres:13.4-alpine
    ports:
      - 5432:5432
    volumes:
      - db-data:/var/lib/postgresql/data
    environment:
      POSTGRES_PASSWORD: pgpass
      POSTGRES_USER: pguser

  app:
    image: ruby:3.0.2
    stdin_open: true
    tty: true
    depends_on:
      - db
    volumes:
      - .:/app
      - bundle:/usr/local/bundle
    environment:
      DB_USER: pguser
      DB_PASS: pgpass
      DB_HOST: db
    working_dir: /app

volumes:
  db-data:
  bundle:

後は上記のQiitaの記事にあるようにRakefileや各種コマンドを作ります。作っているときの実行はdocker-compose run --rm app bashRuby環境に入って逐次実行する形です。

Dockerfileを作る

ある程度プログラムができたらDockerfileを作ってDockerイメージを作成できるようにします。こんな感じ。

FROM ruby:3.0.2-slim

RUN apt-get update && apt-get install -y \
    build-essential \
    libpq-dev  \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /app
COPY Gemfile .
# 使っているGemのバージョン固定のためにlockファイルもbundle installの前にコピー
COPY Gemfile.lock .
RUN bundle install
# RDBMSに対してデータベースを作成したりテーブルを作成したりする
COPY scripts/entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.sh
COPY . .

ENTRYPOINT [ "entrypoint.sh" ]

CMD ["ruby", "scripts/aggregate_todo_count_by_date.rb"]

上記Dockerfile上で作っているentrypoint.shrake db:createrake db:migrateを実行するためのもの。中身は以下のような単純なシェルスクリプトです。

#!/bin/bash

set -e

rake db:create
rake db:migrate

exec "$@"

イメージの作成と実行

Dockerfileを作ったら、

$ docker build -t イメージ名 .

でイメージを作ります。

動作確認には、動作確認用のdocker-compose.ymlを別ディレクトリに作ります。以下のようなものを作りました。

version: "3.8"
services:
  db:
    image: postgres:13.4-alpine
    ports:
      - 5432:5432
    volumes:
      - db-data:/var/lib/postgresql/data
    environment:
      POSTGRES_PASSWORD: pgpass
      POSTGRES_USER: pguser

  app:
    image: イメージ名
    depends_on:
      - db
    environment:
      DB_USER: pguser
      DB_PASS: pgpass
      DB_HOST: db
    working_dir: /app

volumes:
  db-data:

後はdocker-compose upで動かしてあげればOK。

なお、このとき私が書いたRubyプログラムにはputsで進捗を表示するようにしていたのですが...

1_000.times do |i|
  puts "Create TodoItem #{i}"
  # 省略
end

ログを見ても進捗が表示されませんでした。

これは、$stdoutで出力される内容がバッファリングされているためで、同期的に出力する(バッファリングをやめる)にはプログラムの先頭の方で$stdout.sync = trueと書いてあげればOKでした。

docs.ruby-lang.org

ソース

この記事を執筆している時点でのソースコードはこちら。

github.com

Azure Web AppsにてJavaアプリケーションの各種メトリックス情報をApplication Insightsに送る

はじめに

以前、ローカルマシン上で動かしているJavaアプリのメトリックス情報をApplication Insightsに送るということをしました。

miyohide.hatenablog.com

そこから放置していたのですが、ふとしたきっかけでAzure Web Apps上で動かしているJavaアプリのメトリックスの取得について調べる機会があったので、再度調査してみました。

2021年11月現在はポータルで作成時にApplication Insightsとの関連付けができる

以下の更新情報の通り、今はポータルで作成時にApplication Insightsとの関連付けができるようです。

azure.microsoft.com

実際にポータルで作成しようとすると以下の画面が出てきます。

f:id:miyohide:20211106205500p:plain

作成した後、アプリケーション設定を見てみるとApplication Insightsの情報が設定されていました。

f:id:miyohide:20211106205711p:plain

アプリをデプロイする

ソースコードGitHubにデプロイしているので、GitHub Actionsを使ってデプロイしてみます。参考となる記事は以下のもの。

docs.microsoft.com

ちょっとハマったところ

Spring Boot 2.5.6アプリでgradlew buildコマンドを実行すると、build/libs以下にJarファイルが二つ作成されます。このため、GitHub ActionsのAzure/webapps-deploy@v2の処理でDeployment Failed with Error: Error: More than one package matched with specified pattern: build/libs/*.jar. Please restrain the search pattern.と出てデプロイできません。

f:id:miyohide:20211106211048p:plain

Jarファイルが二つ作成される挙動はSpring Boot 2.5からのようで、plainなJarを作成しない方法は

jar {
    enabled = false
}

を設定すれば良いようです。

docs.spring.io

Spring Boot Actuatorの利用

Spring Boot Actuatorを利用しているとそのデータも収集対象としてくれるようなので、build.gradleimplementation 'org.springframework.boot:spring-boot-starter-actuator'も追記しておきます。application.propertiesには特に設定しなくても良さそうです。

docs.microsoft.com

確認する

アプリを動かしてApplication Insightsを見てみると色々とメトリックスが取れているようです。

f:id:miyohide:20211106212340p:plain

f:id:miyohide:20211106212356p:plain

それぞれのパラメータの意味については今後の調査項目ということで(探したんですが、ソースを読むぐらいしか手はない感じ?)。

ソースを読むならmicrometerのソースなのかなと思いました。以下はJVM MemoryのMetricsを実装している部分。

github.com

ソース

本記事を書いた時のソースです。

github.com

Azure Database for PostgreSQLの一時的な接続エラーに対処する その2

はじめに

Azureが提供しているデータベースには、下記のドキュメントにあるように一時的な接続エラーに対処する必要があります。これをSpring Bootで実装してみます。

以下の記事で簡単なリトライ処理については確認しましたが、トランザクションをかけた場合はうまくリトライできませんでした。

miyohide.hatenablog.com

今回はその対応をします。

実装

@Transactionalをつけているメソッドに対して@Retryableをつけてあげます。以下のような感じです。

    @Retryable(maxAttempts = 12, backoff = @Backoff(delay = 5_000, maxDelay = 10_000))
    @Transactional
    public void insert() {
        log.info("Start insert method...");
        // 省略
        log.info("End insert method...");
    }

maxAttemptsdelayなどの値は適宜調整してください。調整のためのヒントとしては、マイクロソフトが公開している上記のドキュメントがあります。マイクロソフトのドキュメントには以下のように書かれています。

初回再試行の前には 5 秒間待機します。

以降再試行するたびに、60 秒を上限として、待ち時間を指数関数的に増やします。

操作が失敗したとアプリケーションで見なされる最大再試行回数を設定します。

これを元に5秒x12回のリトライをするようにしています。

確認

動作を確認してみます。

(省略)
2021-10-26 11:46:08.567  INFO 18 --- [           main] com.github.miyohide.retry.MyService      : Insert record. i = [38], first_name = [Josh00038], last_name = [hogehoge00038]"
2021-10-26 11:46:09.571  INFO 18 --- [           main] com.github.miyohide.retry.MyService      : Insert record. i = [39], first_name = [Josh00039], last_name = [hogehoge00039]"
    at java.base/sun.security.ssl.SSLSocketImpl$AppOutputStream.write(Unknown Source) ~[na:na]"
    at org.springframework.retry.interceptor.RetryOperationsInterceptor$1.doWithRetry(RetryOperationsInterceptor.java:93) ~[spring-retry-1.3.1.jar:na]"
    at com.zaxxer.hikari.pool.ProxyPreparedStatement.executeUpdate(ProxyPreparedStatement.java:61) ~[HikariCP-4.0.3.jar:na]"
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.3.12.jar:5.3.12]"
    at org.springframework.jdbc.core.JdbcTemplate.update(JdbcTemplate.java:960) ~[spring-jdbc-5.3.12.jar:5.3.12]"
    at org.postgresql.jdbc.PgStatement.execute(PgStatement.java:401) ~[postgresql-42.2.24.jar:42.2.24]"
    at org.springframework.retry.interceptor.RetryOperationsInterceptor.invoke(RetryOperationsInterceptor.java:119) ~[spring-retry-1.3.1.jar:na]"
    at com.github.miyohide.retry.RetryApplication.run(RetryApplication.java:22) ~[classes/:na]"
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163) ~[spring-aop-5.3.12.jar:5.3.12]"
    at org.postgresql.core.v3.QueryExecutorImpl.execute(QueryExecutorImpl.java:349) ~[postgresql-42.2.24.jar:42.2.24]"
    at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:791) ~[spring-boot-2.5.6.jar:2.5.6]"
    at com.github.miyohide.retry.MyService$$EnhancerBySpringCGLIB$$30791361.insert(<generated>) ~[classes/:na]"
    at org.springframework.jdbc.core.JdbcTemplate.update(JdbcTemplate.java:1025) ~[spring-jdbc-5.3.12.jar:5.3.12]"
    at org.springframework.retry.support.RetryTemplate.doExecute(RetryTemplate.java:329) ~[spring-retry-1.3.1.jar:na]"
    at org.springframework.retry.annotation.AnnotationAwareRetryOperationsInterceptor.invoke(AnnotationAwareRetryOperationsInterceptor.java:163) ~[spring-retry-1.3.1.jar:na]"
org.postgresql.util.PSQLException: An I/O error occurred while sending to the backend."
    at org.postgresql.core.v3.QueryExecutorImpl.sendSync(QueryExecutorImpl.java:1482) ~[postgresql-42.2.24.jar:42.2.24]"
    at org.springframework.boot.SpringApplication.callRunners(SpringApplication.java:775) ~[spring-boot-2.5.6.jar:2.5.6]"
2021-10-26 11:46:09.592  WARN 18 --- [           main] com.zaxxer.hikari.pool.ProxyConnection   : HikariPool-1 - Connection org.postgresql.jdbc.PgConnection@575b5f7d marked as broke
    at java.base/java.io.BufferedOutputStream.flushBuffer(Unknown Source) ~[na:na]"
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:753) ~[spring-aop-5.3.12.jar:5.3.12]"
    at org.postgresql.jdbc.PgStatement.executeInternal(PgStatement.java:481) ~[postgresql-42.2.24.jar:42.2.24]"
    at java.base/java.io.BufferedOutputStream.flush(Unknown Source) ~[na:na]"
    at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:698) ~[spring-aop-5.3.12.jar:5.3.12]"
    ... 38 common frames omitted"
    at java.base/java.net.SocketOutputStream.socketWrite0(Native Method) ~[na:na]"
Caused by: java.net.SocketException: Connection reset by peer (Write failed)"
    at com.github.miyohide.retry.MyService$$FastClassBySpringCGLIB$$a8112c7.invoke(<generated>) ~[classes/:na]"
    at org.springframework.retry.support.RetryTemplate.execute(RetryTemplate.java:209) ~[spring-retry-1.3.1.jar:na]"
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:753) ~[spring-aop-5.3.12.jar:5.3.12]"
    at com.github.miyohide.retry.RetryApplication.main(RetryApplication.java:14) ~[classes/:na]"
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1332) ~[spring-boot-2.5.6.jar:2.5.6]"
    at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218) ~[spring-core-5.3.12.jar:5.3.12]"
    at org.springframework.jdbc.core.JdbcTemplate.lambda$update$2(JdbcTemplate.java:965) ~[spring-jdbc-5.3.12.jar:5.3.12]"
    at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:388) ~[spring-tx-5.3.12.jar:5.3.12]"
    at java.base/java.net.SocketOutputStream.socketWrite(Unknown Source) ~[na:na]"
    at com.github.miyohide.retry.MyService.insert(MyService.java:33) ~[classes/:na]"
    at com.zaxxer.hikari.pool.HikariProxyPreparedStatement.executeUpdate(HikariProxyPreparedStatement.java) ~[HikariCP-4.0.3.jar:na]"
    at org.postgresql.core.PGStream.flush(PGStream.java:665) ~[postgresql-42.2.24.jar:42.2.24]"
    at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119) ~[spring-tx-5.3.12.jar:5.3.12]"
    at org.postgresql.core.v3.QueryExecutorImpl.execute(QueryExecutorImpl.java:320) ~[postgresql-42.2.24.jar:42.2.24]"
    at org.postgresql.jdbc.PgPreparedStatement.executeUpdate(PgPreparedStatement.java:130) ~[postgresql-42.2.24.jar:42.2.24]"
    at org.postgresql.jdbc.PgPreparedStatement.executeWithFlags(PgPreparedStatement.java:164) ~[postgresql-42.2.24.jar:42.2.24]"
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:345) ~[spring-boot-2.5.6.jar:2.5.6]"
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:753) ~[spring-aop-5.3.12.jar:5.3.12]"
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:783) ~[spring-aop-5.3.12.jar:5.3.12]"
    at org.springframework.jdbc.core.JdbcTemplate.update(JdbcTemplate.java:1015) ~[spring-jdbc-5.3.12.jar:5.3.12]"
    at java.base/sun.security.ssl.SSLSocketOutputRecord.deliver(Unknown Source) ~[na:na]"
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1343) ~[spring-boot-2.5.6.jar:2.5.6]"
    at java.base/java.net.SocketOutputStream.write(Unknown Source) ~[na:na]"
    at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:123) ~[spring-tx-5.3.12.jar:5.3.12]"
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.3.12.jar:5.3.12]"
    at org.springframework.jdbc.core.JdbcTemplate.execute(JdbcTemplate.java:651) ~[spring-jdbc-5.3.12.jar:5.3.12]"
2021-10-26 11:46:10.168  INFO 18 --- [           main] c.g.m.retry.config.RetryableDataSource   : getting connection ..."
2021-10-26 11:46:10.168 DEBUG 18 --- [           main] o.s.retry.support.RetryTemplate          : Retry: count=0"
2021-10-26 11:46:10.170  WARN 18 --- [           main] com.zaxxer.hikari.pool.PoolBase          : HikariPool-1 - Failed to validate connection org.postgresql.jdbc.PgConnection@1b134
2021-10-26 11:46:10.171  WARN 18 --- [           main] com.zaxxer.hikari.pool.PoolBase          : HikariPool-1 - Failed to validate connection org.postgresql.jdbc.PgConnection@7bd96
2021-10-26 11:46:10.172  WARN 18 --- [           main] com.zaxxer.hikari.pool.PoolBase          : HikariPool-1 - Failed to validate connection org.postgresql.jdbc.PgConnection@64dae
2021-10-26 11:46:10.178  WARN 18 --- [           main] com.zaxxer.hikari.pool.PoolBase          : HikariPool-1 - Failed to validate connection org.postgresql.jdbc.PgConnection@1e4c6
2021-10-26 11:46:10.177  WARN 18 --- [           main] com.zaxxer.hikari.pool.PoolBase          : HikariPool-1 - Failed to validate connection org.postgresql.jdbc.PgConnection@1fedf
2021-10-26 11:46:10.179  WARN 18 --- [           main] com.zaxxer.hikari.pool.PoolBase          : HikariPool-1 - Failed to validate connection org.postgresql.jdbc.PgConnection@2ee48
2021-10-26 11:46:10.180  WARN 18 --- [           main] com.zaxxer.hikari.pool.PoolBase          : HikariPool-1 - Failed to validate connection org.postgresql.jdbc.PgConnection@1d901
2021-10-26 11:46:10.181  WARN 18 --- [           main] com.zaxxer.hikari.pool.PoolBase          : HikariPool-1 - Failed to validate connection org.postgresql.jdbc.PgConnection@6d08b
2021-10-26 11:46:10.181  WARN 18 --- [           main] com.zaxxer.hikari.pool.PoolBase          : HikariPool-1 - Failed to validate connection org.postgresql.jdbc.PgConnection@1015a
2021-10-26 11:46:49.206  INFO 18 --- [           main] c.g.m.retry.config.RetryableDataSource   : getting connection ..."
2021-10-26 11:46:49.206 DEBUG 18 --- [           main] o.s.retry.support.RetryTemplate          : Retry: count=1"
2021-10-26 11:46:49.206 DEBUG 18 --- [           main] o.s.retry.support.RetryTemplate          : Checking for rethrow: count=1"
    at com.zaxxer.hikari.pool.HikariProxyPreparedStatement.executeUpdate(HikariProxyPreparedStatement.java) ~[HikariCP-4.0.3.jar:na]"
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.3.12.jar:5.3.12]"
    at com.github.miyohide.retry.MyService$$EnhancerBySpringCGLIB$$30791361.insert(<generated>) ~[classes/:na]"
    at org.springframework.jdbc.core.JdbcTemplate.execute(JdbcTemplate.java:651) ~[spring-jdbc-5.3.12.jar:5.3.12]"
    at org.springframework.jdbc.support.SQLStateSQLExceptionTranslator.doTranslate(SQLStateSQLExceptionTranslator.java:107) ~[spring-jdbc-5.3.12.jar:5.3.12]"
    at org.springframework.jdbc.core.JdbcTemplate.translateException(JdbcTemplate.java:1541) ~[spring-jdbc-5.3.12.jar:5.3.12]"
    at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:79) ~[spring-jdbc-5.3.12.jar:5.3.12]"
    at java.base/java.io.BufferedOutputStream.flush(Unknown Source) ~[na:na]"
    ... 30 common frames omitted"
    at org.postgresql.core.v3.QueryExecutorImpl.sendSync(QueryExecutorImpl.java:1482) ~[postgresql-42.2.24.jar:42.2.24]"
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1343) ~[spring-boot-2.5.6.jar:2.5.6]"
    at org.springframework.jdbc.core.JdbcTemplate.update(JdbcTemplate.java:1015) ~[spring-jdbc-5.3.12.jar:5.3.12]"
    at org.postgresql.core.PGStream.flush(PGStream.java:665) ~[postgresql-42.2.24.jar:42.2.24]"
    at org.postgresql.core.v3.QueryExecutorImpl.execute(QueryExecutorImpl.java:349) ~[postgresql-42.2.24.jar:42.2.24]"
    ... 38 common frames omitted"
    at java.base/java.net.SocketOutputStream.socketWrite0(Native Method) ~[na:na]"
    at org.springframework.retry.interceptor.RetryOperationsInterceptor.invoke(RetryOperationsInterceptor.java:119) ~[spring-retry-1.3.1.jar:na]"
    at org.postgresql.jdbc.PgPreparedStatement.executeUpdate(PgPreparedStatement.java:130) ~[postgresql-42.2.24.jar:42.2.24]"
    at org.springframework.jdbc.core.JdbcTemplate.lambda$update$2(JdbcTemplate.java:965) ~[spring-jdbc-5.3.12.jar:5.3.12]"
    at java.base/java.net.SocketOutputStream.socketWrite(Unknown Source) ~[na:na]"
    at org.springframework.retry.support.RetryTemplate.doExecute(RetryTemplate.java:329) ~[spring-retry-1.3.1.jar:na]"
Caused by: java.net.SocketException: Connection reset by peer (Write failed)"
    at org.springframework.jdbc.core.JdbcTemplate.update(JdbcTemplate.java:1025) ~[spring-jdbc-5.3.12.jar:5.3.12]"
    at org.springframework.jdbc.core.JdbcTemplate.update(JdbcTemplate.java:960) ~[spring-jdbc-5.3.12.jar:5.3.12]"
    at org.postgresql.jdbc.PgPreparedStatement.executeWithFlags(PgPreparedStatement.java:164) ~[postgresql-42.2.24.jar:42.2.24]"
    at com.github.miyohide.retry.RetryApplication.main(RetryApplication.java:14) ~[classes/:na]"
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:345) ~[spring-boot-2.5.6.jar:2.5.6]"
    at java.base/sun.security.ssl.SSLSocketOutputRecord.deliver(Unknown Source) ~[na:na]"
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163) ~[spring-aop-5.3.12.jar:5.3.12]"
    at org.postgresql.core.v3.QueryExecutorImpl.execute(QueryExecutorImpl.java:320) ~[postgresql-42.2.24.jar:42.2.24]"
    at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218) ~[spring-core-5.3.12.jar:5.3.12]"
    at java.base/java.io.BufferedOutputStream.flushBuffer(Unknown Source) ~[na:na]"
    at org.springframework.jdbc.core.JdbcTemplate.execute(JdbcTemplate.java:667) ~[spring-jdbc-5.3.12.jar:5.3.12]"
    at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:70) ~[spring-jdbc-5.3.12.jar:5.3.12]"
    at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:79) ~[spring-jdbc-5.3.12.jar:5.3.12]"
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.3.12.jar:5.3.12]"
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1332) ~[spring-boot-2.5.6.jar:2.5.6]"
    at org.springframework.retry.support.RetryTemplate.execute(RetryTemplate.java:209) ~[spring-retry-1.3.1.jar:na]"
    at com.github.miyohide.retry.RetryApplication.run(RetryApplication.java:22) ~[classes/:na]"
    at com.zaxxer.hikari.pool.ProxyPreparedStatement.executeUpdate(ProxyPreparedStatement.java:61) ~[HikariCP-4.0.3.jar:na]"
    at java.base/java.net.SocketOutputStream.write(Unknown Source) ~[na:na]"
    at org.postgresql.jdbc.PgStatement.execute(PgStatement.java:401) ~[postgresql-42.2.24.jar:42.2.24]"
    at org.postgresql.jdbc.PgStatement.executeInternal(PgStatement.java:481) ~[postgresql-42.2.24.jar:42.2.24]"
Caused by: org.postgresql.util.PSQLException: An I/O error occurred while sending to the backend."
    at org.springframework.retry.interceptor.RetryOperationsInterceptor$1.doWithRetry(RetryOperationsInterceptor.java:93) ~[spring-retry-1.3.1.jar:na]"
    at java.base/sun.security.ssl.SSLSocketImpl$AppOutputStream.write(Unknown Source) ~[na:na]"
    at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:791) ~[spring-boot-2.5.6.jar:2.5.6]"
    at org.springframework.retry.annotation.AnnotationAwareRetryOperationsInterceptor.invoke(AnnotationAwareRetryOperationsInterceptor.java:163) ~[spring-retry-1.3.1.jar:na]"
    at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:698) ~[spring-aop-5.3.12.jar:5.3.12]"
    at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:123) ~[spring-tx-5.3.12.jar:5.3.12]"
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:783) ~[spring-aop-5.3.12.jar:5.3.12]"
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:753) ~[spring-aop-5.3.12.jar:5.3.12]"
2021-10-26 11:46:51.627 ERROR 18 --- [           main] o.s.t.i.TransactionInterceptor           : Application exception overridden by rollback exception"
    at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119) ~[spring-tx-5.3.12.jar:5.3.12]"
    at com.github.miyohide.retry.MyService$$FastClassBySpringCGLIB$$a8112c7.invoke(<generated>) ~[classes/:na]"
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:753) ~[spring-aop-5.3.12.jar:5.3.12]"
    at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:388) ~[spring-tx-5.3.12.jar:5.3.12]"
org.springframework.dao.DataAccessResourceFailureException: PreparedStatementCallback; SQL [INSERT INTO customers(first_name, last_name) VALUES (?, ?)]; An I/O error occurred while 
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:753) ~[spring-aop-5.3.12.jar:5.3.12]"
    at org.springframework.boot.SpringApplication.callRunners(SpringApplication.java:775) ~[spring-boot-2.5.6.jar:2.5.6]"
    at com.github.miyohide.retry.MyService.insert(MyService.java:33) ~[classes/:na]"
2021-10-26 11:46:59.489 DEBUG 18 --- [           main] o.s.retry.support.RetryTemplate          : Retry: count=0"
2021-10-26 11:46:59.490  INFO 18 --- [           main] c.g.m.retry.config.RetryableDataSource   : getting connection ..."
2021-10-26 11:46:59.488 DEBUG 18 --- [           main] o.s.retry.support.RetryTemplate          : Checking for rethrow: count=1"
2021-10-26 11:46:59.488 DEBUG 18 --- [           main] o.s.retry.support.RetryTemplate          : Retry: count=1"
2021-10-26 11:46:59.498  INFO 18 --- [           main] com.github.miyohide.retry.MyService      : Insert record. i = [0], first_name = [Josh00000], last_name = [hogehoge00000]"
2021-10-26 11:46:59.497  INFO 18 --- [           main] com.github.miyohide.retry.MyService      : Start insert method..."
2021-10-26 11:47:01.033  INFO 18 --- [           main] com.github.miyohide.retry.MyService      : Insert record. i = [1], first_name = [Josh00001], last_name = [hogehoge00001]"
2021-10-26 11:47:02.038  INFO 18 --- [           main] com.github.miyohide.retry.MyService      : Insert record. i = [2], first_name = [Josh00002], last_name = [hogehoge00002]"
2021-10-26 11:47:03.042  INFO 18 --- [           main] com.github.miyohide.retry.MyService      : Insert record. i = [3], first_name = [Josh00003], last_name = [hogehoge00003]"
2021-10-26 11:47:04.048  INFO 18 --- [           main] com.github.miyohide.retry.MyService      : Insert record. i = [4], first_name = [Josh00004], last_name = [hogehoge00004]"
2021-10-26 11:47:05.052  INFO 18 --- [           main] com.github.miyohide.retry.MyService      : Insert record. i = [5], first_name = [Josh00005], last_name = [hogehoge00005]"

org.postgresql.util.PSQLException: An I/O error occurred while sending to the backend.が発生したり、java.net.SocketException: Connection reset by peer (Write failed)が発生したりしていますが、2021-10-26 11:46:59.498時点で再度0からInsert処理が始まりました。無事リトライ処理が行われているようです。

無事、一時的な接続エラーに対処することができました。

実装ソース

ソースは以下に置いてあります。

github.com

non_retryフォルダにあるものはリトライ処理を実装していないもの、retryフォルダにあるのがリトライ処理を実装したものになります。