PostgreSQL constraint を書かない時の UPSERT

かなり昔、
oboe2uran.hatenablog.com

を書いた。
constraint を書かない時の UPSERT、Primary Key しか書いていない時の
UPSERT の書き方は、
前回の投稿のテーブルで、id が Primary Keyである場合、、、
前回の投稿

INSERT INTO sample (id, `point`, price) VALUES (3, 40, 400)
ON CONFLICT ON CONSTRAINT sample_pkey
DO UPDATE SET point = 40, price = 400

constraint を書かない時

INSERT INTO sample (id, `point`, price) VALUES (3, 40, 400)
ON CONFLICT (id)
DO UPDATE SET point = 40, price = 400

となる。
複数列で、Primary Key なら、カンマ区切りで並べる。

AWS Lambda タイムアウトの捕捉

AWS Lambda のタイムアウトJava で作成するLambda関数に捕捉させる要件。
こういう要件、なんで Python で作らないんだという声は置いといて。。。
タイムアウトする Lambda は、ログ出力は、awslogs で CloudWatch で閲覧が可能という前提

Lambda のタイムアウトAWSが、"Task timed out" のログを出す
⇒ Cloud Watch Log をトリガーに、フィルターで、"Task timed out" を捕捉
⇒ トリガー設定した Lambda にイベント通知で該当ログ "Task timed out" と
  ロググループ、ログストリーム名が送られてくる
⇒ Cloud Watch を参照する Client を作成してタイムアウト発生したログを参照

Cloud Watch を参照する Client は、AWS SDK-Java version 2 を使うので、
Maven 依存関係

<dependency>
   <groupId>software.amazon.awssdk</groupId>
   <artifactId>cloudwatchlogs</artifactId>
   <version>2.17.168</version>
</dependency>

Cloud Watch Log をトリガーから、タイムアウト捕捉するLamnda に送られてくるデータ(JSON)は。
gzip 圧縮されて更に Base64 エンコードされたデータで、Lambdaリクエストハンドラに渡されるので、gzip 解凍の為に
Apache common の commons-compress を使って gzip 解凍する

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-compress</artifactId>
    <version>1.21</version>
</dependency>

あとついでに、以下、パターンマッチを簡単に書くために
ReturnalConsumer を使いたくて、、

<dependency>
        <groupId>org.yipuran.core</groupId>
        <artifactId>yipuran-core</artifactId>
        <version>4.32</version>
</dependency>

必要なインポート

import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.cloudwatchlogs.CloudWatchLogsClient;
import software.amazon.awssdk.services.cloudwatchlogs.model.GetLogEventsRequest;
import org.yipuran.function.ReturnalConsumer;

タイムアウト捕捉する Lambda Handler

public class TimeoutCatchHandler implements RequestHandler<InputStream, String>{
    private Logger logger = LoggerFactory.getLogger(this.getClass());
    @Override
    public String handleRequest(InputStream input, Context context){
        ObjectMapper mapper = new ObjectMapper();
        try{
            Map<String, Map<String, Object>> map = mapper.readValue(input, new TypeReference<Map<String, Map<String, Object>>>(){});
            String data = map.get("awslogs").get("data").toString();
            logger.info("map  = " + map);
            String json = readLog(Base64.getDecoder().decode(data));
            logger.info("json = " + json);
            AwsLogCatch logcatch = mapper.readValue(json, new TypeReference<AwsLogCatch>(){});
            String message = logcatch.getLogEvents().get(0).getMessage();

            logger.info("messageType = " + logcatch.getMessageType());
            logger.info("logGroup    = " + logcatch.getLogGroup());
            logger.info("logStream   = " + logcatch.getLogStream());
            logger.info("getLogEvents() = " + logcatch.getLogEvents());
            logger.info("id = " + logcatch.getLogEvents().get(0).getId());
            logger.info("timestamp = " + logcatch.getLogEvents().get(0).getTimestamp());
            logger.info("message = " + message);

            int begin = ReturnalConsumer.of(Matcher.class).with(Matcher::find).apply(Pattern.compile(
                    "\\d{4}\\-(0[1-9]|1[012])\\-(0[1-9]|[12][0-9]|3[01])T(0[0-9]|1[0-9]|2[0-3]):(0[0-9]|[0-5][0-9]):(0[0-9]|[0-5][0-9])\\.[0-9]{3}Z"
            ).matcher(message)).end();
            int end = ReturnalConsumer.of(Matcher.class).with(Matcher::find).apply(Pattern.compile("Task timed out").matcher(message)).start();
            String requestId = message.substring(begin, end).trim();

            logger.info("requestId = " + requestId);

            CloudWatchLogsClient client = CloudWatchLogsClient.builder().region(Region.of”ap-northeast-1”)).build();
            GetLogEventsRequest getLogEventsRequest = GetLogEventsRequest.builder()
                    .logGroupName(logcatch.getLogGroup())
                    .logStreamName(logcatch.getLogStream())
                    .startFromHead(true)
                    .build();
            
            client.getLogEvents(getLogEventsRequest).events().stream().forEach(event->{
                logger.info(event.message());
            });
        }catch(IOException e){
            logger.error(e.getMessage(), e);
        }
        return null;
    }
    private String readLog(byte[] input) {
        try(ByteArrayInputStream bin = new ByteArrayInputStream(input);GzipCompressorInputStream gin = new GzipCompressorInputStream(bin)){
            ByteArrayOutputStream bo = new ByteArrayOutputStream();
            int size = 0;
            byte[] buf = new byte[1024];
            while((size = gin.read(buf)) > 0){
                bo.write(buf, 0, size);
                bo.flush();
            }
            bo.close();
            return new String(bo.toByteArray(), StandardCharsets.UTF_8);
        }catch(IOException e){
            e.printStackTrace();
            throw new RuntimeException(e);
        }
    }
}

トリガーが送ったイベントデータは、RequestHandler InputStream で受け取るデータの "data"キーで受け取った
データ JSON が、gzip を圧縮されて Base64 エンコードされているので、
private String readLog(byte[] input) のメソッドで、Base64 デコードしたデータから
gzip 解凍して抽出したJSONを、以下の 読みやすい解析のための任意のクラスに、
Jacksonで変換する。

import java.io.Serializable;
import java.util.List;
import lombok.Data;
/**
 * AwsLogCatch
 */
@Data
public class AwsLogCatch implements Serializable{
    private static final long serialVersionUID = 1L;
    private String messageType;
    private String owner;
    private String logGroup;
    private String logStream;
    private List<String> subscriptionFilters;
    private List<AwsLogEvent> logEvents;
}
import java.io.Serializable;
import lombok.Data;
/**
 * CloudWatch トリガー フィルターに引っ掛かったログ
 */
@Data
public class AwsLogEvent implements Serializable{
    private static final long serialVersionUID = 1L;
    private String id;
    private long timestamp;
    private String message;
}

これが、トリガー フィルターに引っ掛かったログを解析するもので、

AwsLogCatch logcatch = mapper.readValue(json, new TypeReference<AwsLogCatch>(){});

Cloud Watch を参照する Client は、AWS SDK-Java version 2 は、
CloudWatchLogsClient client を生成して、logGroup , logStream を指定して
読み込むのである。
必要なロール権限ポリシーは、CloudWatchLogsReadOnlyAccess


PythonCloudWatchLogs — Boto3 Docs 1.21.42 documentation
に相当するクライアントで GetLogEventsRequest を作って
CloudWatchLogsClient の getLogEventsメソッドの evebts()でようやくタイムアウト発生した Lambda のログを
参照することができる。

上の

      logger.info("timestamp = " + logcatch.getLogEvents().get(0).getTimestamp());

で取得している時刻は、epoc ミリ秒なので

long epocmili = Long.parseLong(logcatch.getLogEvents().get(0).getTimestamp()));
LocalDateTime time = Instant.ofEpochMilli(epocmili).atZone(ZoneId.of("Asia/Tokyo")).toLocalDateTime();

とすれば、LocalDateTime を見ることができるだろう。

ReturnalConsumer の使いどころ

先日と同じことではあるが、、
Matcher の find() と group() (2) - Oboe吹きプログラマの黙示録

ReturnalConsumer の使いどころとして、、
https://github.com/yipuran/yipuran-core/wiki#returnalconsumert

String str = "aaa134_cd45_def";
String res = ReturnalConsumer.of(Matcher.class).with(Matcher::find).get(Pattern.compile("\\d+").matcher(str)).group();

この結果は、res = "134" である。

課題:"aaa_3", "bbb_12", "ccc", "ddd_32zz", "eee_7" という文字列のリストで、末尾が、"_"+数
中で最大値を求める。

Pattern taildec = Pattern.compile("^.+_\\d+$");
Pattern decp = Pattern.compile("_\\d+$");

Integer max = List.of("aaa_3", "bbb_12", "ccc", "ddd_32zz", "eee_7")
.stream().filter(e->taildec.matcher(e).matches())
.map(e->ReturnalConsumer.of(Matcher.class).with(Matcher::find).get(decp.matcher(e)).group().substring(1))
.map(Integer::parseInt)
.max(Comparator.naturalOrder()).get();

これで結果、Integer max の値は 12 である。

もし、末尾が、"_"+数 の要素が存在しないリストを
考慮するなら、、
 .max(Comparator.naturalOrder()).get();
の部分は、
 .max(Comparator.naturalOrder()).orElse(null);
にする必要があり、 Integer 型を結果にすべきである。

Matcher の find() と group() (2)

oboe2uran.hatenablog.com

を書きましたが、Matcher だけにフォーカスすれば、
必ずマッチが約束されているなら、

Git-Hub にUPした https://github.com/yipuran/yipuran-core/wiki#returnalconsumert
ReturnalConsumer を使って

Pattern ptn = Pattern.compile("\\d{4}(0[1-9]|1[012])(0[1-9]|[12][0-9]|3[01])(0[0-9]|1[0-9]|2[0-3])(0[0-9]|[0-5][0-9])(0[0-9]|[0-5][0-9])");

String timestring = ReturnalConsumer.of(Matcher.class)
                                .with(e->e.find())
                                .get(ptn.matcher(string))
                                .group();

とするのも良いでしょう。

Matcher の find() と group() (1)

java.util.regex.Matcher は、マッチをさせてから、一致部分を抽出というステップが
煩わしいと思っていた。

例えばファイル名の一部が時刻文字列(yyyyMMddHHmmss)になっており、時刻まで求めるのに、
java.util.regex.Pattern 正規表現でマッチー>Matcher で find() を実行させてから、
group() で抽出ー>LocalDateTime へ。
なんて手間で、Matcher 変数を持たなければならないカッコ悪さがある。
工夫しようと考えても。。

BiFunction<File, Pattern, LocalDateTime> fileNameToDateTime = (file, p)->{
    Matcher m = p.matcher(file.getName());
    m.find();
    return LocalDateTime.parse(m.group(), DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));
};
Pattern ptn = Pattern.compile("\\d{4}(0[1-9]|1[012])(0[1-9]|[12][0-9]|3[01])(0[0-9]|1[0-9]|2[0-3])(0[0-9]|[0-5][0-9])(0[0-9]|[0-5][0-9])");
List<File> list = 
Arrays.stream(new File(path).listFiles(f->ptn.matcher(f.getName()).matches()))
.sorted((a, b)->fileNameToDateTime.apply(b, ptn2).compareTo(fileNameToDateTime.apply(a, ptn)))
.collect(Collectors.toLis());

Optional を使う

LocalDateTime d1 = Optional.ofNullable(ptn.matcher(string))
.map(m->m.find() ? LocalDateTime.parse(m.group(), DateTimeFormatter.ofPattern("yyyyMMddHHmmss")) : null)
.orElse(null);

Maven package 実行のメモ

テストスキップしてパッケージ生成
mvn package -DskipTests=true

クリーンしてからパッケージ(よく使いそうな実行)
mvn clean package -DskipTests=true

Maven プロジェクトーサブモジュール構成で、モジュールを指定する場合
書式は、、
mvn clean package -pl {module} -am

-pl --project の省略形である。
-am a--also-make の省略形である。

a--also-make  :  依存する他のサブモジュールも一緒に処理する。
付けない場合、-plで指定したモジュールだけが対象になる

親プロジェクトで、alpha というモジュールを clean して、テストスキップでパッケージ生成する場合、、
mvn clean package -DskipTests=true -pl alpha -am