Javaのバージョンは最新に追随するか、LTSを乗り換えていくか

先日こんなことをつぶやいていた

どういうことかというと、最新バージョンに追随しようとした場合、buildpacksやもしかしたらその他のFWやライブラリがEOLになったjdkのサポートを直ちにきって、ほんの短い間バージョンを塩漬けにしておきたいと思ってもできない、という状況が起こり得るということ。

少なくともbuildpacksを利用する場合は、LTSを乗り換えていく戦略を取るほうが安全そう。

jdkのリリース後、すぐに乗り換えられるだけの体制が整っていれば別だが、そうじゃないならLTSを乗り換えていきましょう、というお話でした。

Spring Bootのバージョンを2.4.4にあげるときの落とし穴

概要

Spring Bootのバージョンを2.4.4に上げたタイミングで自動的に解決されるmysql-connector-javaのバージョンが8.0.23に変わります。

しかし、このバージョンからタイムゾーンを接続文字列に含めて明示してない(serverTimezoneなどを指定していない)場合に採用されるタイムゾーンが8.0.22以前はサーバ側(MySQLインスタンスの設定)だったにもかかわらず、8.0.23からは接続元(Spring Bootが動いている環境のタイムゾーン)に変わるので、日時や日付をLocalDateやLocalDateTimeを使っていると9時間ずれて動いてしまいます。

対応策

手っ取り早いのはserverTimezoneまたはconnectionTimeZoneを指定し、MySQLが動いている環境のタイムゾーンと一致させることっぽいです。いやはやハマった…

f:id:kogayushi:20210409222810p:plain

情報はここ dev.mysql.com

MDCInsertingServletFilterとSentryAppenderを組み合わせると一部のMDCのvalueがnullになっているせいで、処理途中でNPEになってしまいsentryにイベント通知できない

掲題の通りの事象に遭遇して、解決に手間取ったのでメモを残しておきます。

その時ぼやき半分につぶやいていたのはこちら。

MDCInsertingServletFilterからすると、valuenullをセットするのは正当な気がしています。

問題が顕在化している箇所はSentryAppenderがMDCの値をコピーする箇所であり、具体的にはCollectionUtils#shallowCopyを呼び出していてその中でnew ConcurrentHashMap<>(map)とするときに、ConcurrentHashMapのコンストラクタ内のガード節でnullチェックをしており、nullの場合はNPEをthrowしてしまっています。

MDCのvaluenullの場合は、空文字に変換すればよいのでは?と一瞬思いましたが、これはデータの改ざん(not加工)であるような気もしており、筋が悪そうです。

何らかの方法でConcurrentHashMapを辞めれれば良さそうな気がします。1

workaroundですがMDCInsertingServletFilterをコピペしてCustomMDCInsertingServletFilterという名前で作り、valuenullになりうる箇所はnullの場合に空文字にするという対応にしました。

package sample

import ch.qos.logback.classic.ClassicConstants
import org.slf4j.MDC
import java.io.IOException
import javax.servlet.Filter
import javax.servlet.FilterChain
import javax.servlet.FilterConfig
import javax.servlet.ServletException
import javax.servlet.ServletRequest
import javax.servlet.ServletResponse
import javax.servlet.http.HttpServletRequest

// オリジナルはch.qos.logback.classic.helpers.MDCInsertingServletFilter
// 値がないときはMDCのvalueにnullをputするが、それだとSentryAppender#createEvent内で、
// CollectionUtils.shallowCopyを使ってMDCの値をコピーしようとしているが、
// ConcurrentHashMapがnullのvalueを受け付けないので、NPEを送出してしまう
// OSS側で直してほしい気もするが、正しい挙動がなく直しようがなさそう
open class CustomMDCInsertingServletFilter : Filter {

    override fun destroy() {
        // do nothing
    }

    @Throws(IOException::class, ServletException::class)
    override fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) {
        insertIntoMDC(request)
        try {
            chain.doFilter(request, response)
        } finally {
            clearMDC()
        }
    }

    private fun insertIntoMDC(request: ServletRequest) {
        MDC.put(ClassicConstants.REQUEST_REMOTE_HOST_MDC_KEY, request.remoteHost)
        if (request is HttpServletRequest) {
            val httpServletRequest = request
            MDC.put(ClassicConstants.REQUEST_REQUEST_URI, httpServletRequest.requestURI)
            val requestURL = httpServletRequest.requestURL
            if (requestURL != null) {
                MDC.put(ClassicConstants.REQUEST_REQUEST_URL, requestURL.toString())
            }
            MDC.put(ClassicConstants.REQUEST_METHOD, httpServletRequest.method)
            MDC.put(ClassicConstants.REQUEST_QUERY_STRING, httpServletRequest.queryString ?: "") // nullになりうるのでnullだった場合は空文字に置換
            MDC.put(ClassicConstants.REQUEST_USER_AGENT_MDC_KEY, httpServletRequest.getHeader("User-Agent") ?: "") // nullになりうるのでnullだった場合は空文字に置換
            MDC.put(ClassicConstants.REQUEST_X_FORWARDED_FOR, httpServletRequest.getHeader("X-Forwarded-For") ?: "") // nullになりうるのでnullだった場合は空文字に置換
        }
    }

    private fun clearMDC() {
        MDC.remove(ClassicConstants.REQUEST_REMOTE_HOST_MDC_KEY)
        MDC.remove(ClassicConstants.REQUEST_REQUEST_URI)
        MDC.remove(ClassicConstants.REQUEST_QUERY_STRING)
        // removing possibly inexistent item is OK
        MDC.remove(ClassicConstants.REQUEST_REQUEST_URL)
        MDC.remove(ClassicConstants.REQUEST_METHOD)
        MDC.remove(ClassicConstants.REQUEST_USER_AGENT_MDC_KEY)
        MDC.remove(ClassicConstants.REQUEST_X_FORWARDED_FOR)
    }

    @Throws(ServletException::class)
    override fun init(arg0: FilterConfig) {
        // do nothing
    }
}

これググっても全くHITせず、OSS側のコードをデバッグしないと突き止められませんでした。

MDCInsertingServletFilterSentryAppenderを組み合わせて使っている事例は多い気がするので同じように困ってる人の役にたつ情報になってくれれば幸いです。

2021/04/06追記

ちょうどこの記事の投稿の直後ぐらいに問題が報告されて修正が入ったようです。 @wreulickeさん情報提供頂きありがとうございます!


  1. 執筆時点ではまだソースコードを解析していないため、影響範囲が不明です。

Atomic Designの段階の分類の指針について考えていること(2020/6/10バージョン)

概要

掲題についてだらだらと考えたことを表にまとめたのでメモに残しておきます。

大筋としてはそんなに間違えていないと思いますが、兼業フロントエンジニアなのであまり信じすぎないでください…。

前提

nuxt.js

分類とその説明

分類 該当フォルダ 説明
atoms components/atoms 原則一つのタグで表現できるもの。
標準のHTMLタグの見た目を装飾したり、
なんらかのアクションのトリガーを追加したものになる傾向が強い。
molecules components/molecules 2つ以上のタグで構成される意味のあるまとまり。
API呼び出しやvuexの更新といった外部の状態を直接操作するような振る舞いはない。
ルーセルやバリデーション込みの入力フィールドなどが該当する。
organisms components/organisms 2つ以上のタグで構成される意味のあるまとまり。
フォームやモーダル、テーブルが該当する。
コンポーネントを組合せて共闘動作させるための状態を持つことができる。
外部の状態を操作する振る舞いを持つことができる。
状態や振る舞いを持ってなくてもorganismsとして成立する
作成するコンポーネントをどれに分類するかで一番悩むもののはず。。
templates components/layouts organisms/molecules/atomsを配置する。
nuxtのlayoutsと混同しないように注意。
乱暴だが、いわゆるプレゼンテーショナルコンポーネントの考え方に近い。
単体で完結したorganismsのみで構成される場合、省略可(になるはず)
pages pages templateに、状態を流し込むのが役割。
これも乱暴だがコンテナコンポーネントの考え方に近い。
単体で完結したorganismsのみで構成される場合、
ただtemplatesを呼び出すだけになるのでその場合はtemplatesを省略可。

特徴と段階のマトリクス

項目 atoms molecules oarganisms templates pages 備考
振る舞い ない ない ある ない ある 一見、moleclesに見えても振る舞い(ビジネスロジック)があるならorganismsにする
分割可否 不可 これ以上分割できないものはatoms。
ただし、他のatomsのベースになりうるatomsはあり得るかもしれない。
例えばボタンコンポーネントに共通して備えさせたいpropsを定義するためのスーパータイプなど
標準のHTMLタグの数 原則1つ 2つ以上 2つ以上 2つ以上 2つ以上
コンポーネント組合せ数 n/a 2つ以上 2つ以上 2つ以上 2つ以上

io.lettuce.core.RedisCommandExecutionException: ERR no such key

以下のようなエラーを見かけたので調べてみた。

io.lettuce.core.RedisCommandExecutionException: ERR no such key
    at io.lettuce.core.ExceptionFactory.createExecutionException(ExceptionFactory.java:135)
    at io.lettuce.core.ExceptionFactory.createExecutionException(ExceptionFactory.java:108)
    at io.lettuce.core.protocol.AsyncCommand.completeResult(AsyncCommand.java:120)
    at io.lettuce.core.protocol.AsyncCommand.complete(AsyncCommand.java:111)
    at io.lettuce.core.protocol.CommandWrapper.complete(CommandWrapper.java:59)
    at io.lettuce.core.protocol.CommandHandler.complete(CommandHandler.java:646)
    at io.lettuce.core.protocol.CommandHandler.decode(CommandHandler.java:604)
    at io.lettuce.core.protocol.CommandHandler.channelRead(CommandHandler.java:556)
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362)
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348)
    at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:340)
    at io.netty.channel.ChannelInboundHandlerAdapter.channelRead(ChannelInboundHandlerAdapter.java:86)
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362)
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348)
    at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:340)
    at io.netty.channel.ChannelInboundHandlerAdapter.channelRead(ChannelInboundHandlerAdapter.java:86)
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362)
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348)
    at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:340)
    at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1434)
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362)
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348)
    at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:965)
    at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:163)
    at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:648)
    at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:583)
    at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:500)
    at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:462)
    at io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:897)
    at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
    at java.lang.Thread.run(Thread.java:834)
org.springframework.data.redis.RedisSystemException: Error in execution; nested exception is io.lettuce.core.RedisCommandExecutionException: ERR no such key
    at org.springframework.data.redis.connection.lettuce.LettuceExceptionConverter.convert(LettuceExceptionConverter.java:54)
    at org.springframework.data.redis.connection.lettuce.LettuceExceptionConverter.convert(LettuceExceptionConverter.java:52)
    at org.springframework.data.redis.connection.lettuce.LettuceExceptionConverter.convert(LettuceExceptionConverter.java:41)
    at org.springframework.data.redis.PassThroughExceptionTranslationStrategy.translate(PassThroughExceptionTranslationStrategy.java:44)
    at org.springframework.data.redis.FallbackExceptionTranslationStrategy.translate(FallbackExceptionTranslationStrategy.java:42)
    at org.springframework.data.redis.connection.lettuce.LettuceConnection.convertLettuceAccessException(LettuceConnection.java:268)
    at org.springframework.data.redis.connection.lettuce.LettuceKeyCommands.convertLettuceAccessException(LettuceKeyCommands.java:817)
    at org.springframework.data.redis.connection.lettuce.LettuceKeyCommands.rename(LettuceKeyCommands.java:325)
    at org.springframework.data.redis.connection.DefaultedRedisConnection.rename(DefaultedRedisConnection.java:118)
    at org.springframework.data.redis.core.RedisTemplate.lambda$rename$16(RedisTemplate.java:932)
    at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:224)
    at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:184)
    at org.springframework.data.redis.core.RedisTemplate.rename(RedisTemplate.java:931)
    at org.springframework.session.data.redis.RedisOperationsSessionRepository$RedisSession.saveChangeSessionId(RedisOperationsSessionRepository.java:867)
    at org.springframework.session.data.redis.RedisOperationsSessionRepository$RedisSession.saveDelta(RedisOperationsSessionRepository.java:826)
    at org.springframework.session.data.redis.RedisOperationsSessionRepository$RedisSession.access$000(RedisOperationsSessionRepository.java:703)
    at org.springframework.session.data.redis.RedisOperationsSessionRepository.save(RedisOperationsSessionRepository.java:421)
    at org.springframework.session.data.redis.RedisOperationsSessionRepository.save(RedisOperationsSessionRepository.java:247)
    at org.springframework.session.web.http.SessionRepositoryFilter$SessionRepositoryRequestWrapper.commitSession(SessionRepositoryFilter.java:240)
    at org.springframework.session.web.http.SessionRepositoryFilter$SessionRepositoryRequestWrapper.access$100(SessionRepositoryFilter.java:201)
    at org.springframework.session.web.http.SessionRepositoryFilter.doFilterInternal(SessionRepositoryFilter.java:154)
    at org.springframework.session.web.http.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:81)
    at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1610)
    at org.springframework.cloud.sleuth.instrument.web.ExceptionLoggingFilter.doFilter(ExceptionLoggingFilter.java:50)
    at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1610)
    at brave.servlet.TracingFilter.doFilter(TracingFilter.java:65)
    at org.springframework.cloud.sleuth.instrument.web.LazyTracingFilter.doFilter(TraceWebServletAutoConfiguration.java:145)
    at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1610)
    at org.eclipse.jetty.servlet.ServletHandler.doHandle(ServletHandler.java:540)
    at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:146)
    at org.eclipse.jetty.security.SecurityHandler.handle(SecurityHandler.java:566)
    at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:132)
    at org.eclipse.jetty.server.handler.ScopedHandler.nextHandle(ScopedHandler.java:257)
    at org.eclipse.jetty.server.session.SessionHandler.doHandle(SessionHandler.java:1588)
    at org.eclipse.jetty.server.handler.ScopedHandler.nextHandle(ScopedHandler.java:255)
    at org.eclipse.jetty.server.handler.ContextHandler.doHandle(ContextHandler.java:1345)
    at org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:203)
    at org.eclipse.jetty.servlet.ServletHandler.doScope(ServletHandler.java:480)
    at org.eclipse.jetty.server.session.SessionHandler.doScope(SessionHandler.java:1557)
    at org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:201)
    at org.eclipse.jetty.server.handler.ContextHandler.doScope(ContextHandler.java:1247)
    at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:144)
    at org.eclipse.jetty.server.Dispatcher.forward(Dispatcher.java:227)
    at org.eclipse.jetty.server.Dispatcher.error(Dispatcher.java:86)
    at org.eclipse.jetty.server.handler.ErrorHandler.doError(ErrorHandler.java:119)
    at org.eclipse.jetty.server.handler.ErrorHandler.handle(ErrorHandler.java:78)
    at org.springframework.boot.web.embedded.jetty.JettyEmbeddedErrorHandler.handle(JettyEmbeddedErrorHandler.java:55)
    at org.eclipse.jetty.server.Response.sendError(Response.java:655)
    at org.eclipse.jetty.server.handler.AbstractHandler.doError(AbstractHandler.java:100)
    at org.eclipse.jetty.server.handler.ContextHandler.doHandle(ContextHandler.java:1339)
    at org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:203)
    at org.eclipse.jetty.servlet.ServletHandler.doScope(ServletHandler.java:480)
    at org.eclipse.jetty.server.session.SessionHandler.doScope(SessionHandler.java:1557)
    at org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:201)
    at org.eclipse.jetty.server.handler.ContextHandler.doScope(ContextHandler.java:1247)
    at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:144)
    at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:132)
    at org.eclipse.jetty.server.Server.handle(Server.java:502)
    at org.eclipse.jetty.server.HttpChannel.handle(HttpChannel.java:419)
    at org.eclipse.jetty.server.HttpConnection.onFillable(HttpConnection.java:260)
    at org.eclipse.jetty.io.AbstractConnection$ReadCallback.succeeded(AbstractConnection.java:305)
    at org.eclipse.jetty.io.FillInterest.fillable(FillInterest.java:103)
    at org.eclipse.jetty.io.ChannelEndPoint$2.run(ChannelEndPoint.java:118)
    at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.runTask(EatWhatYouKill.java:333)
    at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.doProduce(EatWhatYouKill.java:310)
    at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.tryProduce(EatWhatYouKill.java:168)
    at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.run(EatWhatYouKill.java:126)
    at org.eclipse.jetty.util.thread.ReservedThreadExecutor$ReservedThread.run(ReservedThreadExecutor.java:366)
    at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:765)
    at org.eclipse.jetty.util.thread.QueuedThreadPool$2.run(QueuedThreadPool.java:683)
    at java.lang.Thread.run(Thread.java:834)

どうも、spring boot 2.1.x系のspring sessionとspring data redisの組み合わせを使っていると出るらしい。 issueも報告されている

以下の通り、spring boot 2.3.x系では直っているので、解決策はバージョンの最新化をすれば良さそう

github.com

これ日本語情報が全く無かったので、英語が苦手な人だと困りはてる気がしたのでキーワードだけでもと思いブログに情報を残しておきます。

テストコードを書くその前に

今日テストコードを書く前にどんな事を考えているかつぶやいたのでブログにも転記しておく。

要約するとテストも戦略を考えて設計してから書こうね、というお話でした。

私が考えるファーストクラスコレクションを定義しておく意味や意義みたいなもの

私の考え

私は、ドメインオブジェクトのコレクションには常にファーストクラスコレクションを定義しておくと良いと考えています。

例えば、List<DomainObject>という生の配列をクライアントコードに操作させる場合、あちこちのクライアントコードに同じ操作が(しかもちょっとずつ違うやり方で)書かれているという状況を招きかねないためです。

特に別の技術者が参画してきたときはまだコードの書き方を真似しきれないはずですから、その状況を引き起こしやすいでしょう。

例えば、List<DomainObject>を許した場合

どういうことがおきるでしょうか。

同じ操作なのに書き方が違うものがあちこちのクライアントコードに散在しているとします。 影響をうけうるクライアントコードをすべて特定するのが手間です。 また、特定できたとして書き方が違うと本当にそこにも変更を反映すべきなのかどうか迷ってしまうかもしれません、それを調べるコストがもったいないと感じてしまいます。

また、別のケースとしては、属性に変更があったから操作に影響が出たのでとある処理の修正が必要といったときに、その変更が発散していろんなクライアントコードに波及してしまいます。

一方で、ファーストクラスコレクションを定義してそこに操作を集約しておくとどうなるか

操作を実装されるコード間の距離が物理的に近いのでそのコレクションを操作する時の書き方の統一感を期待できますし、何かを操作するコードが1箇所であることを期待できるので、変更の発散がかなり抑制できるはず1です。

開発に関わるエンジニアが少数しかおらず、その入れ替わりがないのであればまだ操作が存在しないコレクションまでファーストクラスコレクションとするのは短期的にはコスパは悪いかもしれません。

しかし、先述の理由から中長期的にはコスパ良いと私は信じています。

なお、ファーストクラスコレクションに限りませんが、私の経験則では、後からリファクタリングして対応するという方針にする場合、似たような処理(分岐)が2箇所で出てきたら黄信号(リファクタリングの必要が高い)、3箇所になったら赤信号(このタイミングでリファクタリングしておかないと高い確率でまずい状況を引き起こす)って感じています。


  1. 運が良ければ完全になくせるかもしれませんが、変更はどうしても波及するものです。しかし、その影響度合いを小さくすることは可能だと考えています。