Search code examples
keycloakquarkushazelcastvert.x

Keycloak 21.1.2 with Quarkus and VertX clustered (through hazelcast) throws NoSuchMethodError VertxInternal.getWorkerPool


I am trying to get a custom provider within Keycloak to work with a clustered VertX solution. And have been breaking my head on the problem for the past week. In older version of Keycloak this didn't use to be an issue, but since the introduction of Quarkus and with it, its bundled version of VertX I have been unable to get things working.

There are always two outcomes,

  • A: Either I load Vertx within a packaged `shadowJar and Quarkus complains about duplicate interfaces for which I can't find a workaround (if renaming the packages on build is possible, I'd love to know).
  • Or B: I use the bundled VertX to inject it within an SPI through Arc.Container() and get the correct VertX which correctly joins the Hazelcast cluster but throws an Exception as soon as I try to send a message to the EventBus using for example publish():

Caused by: java.lang.NoSuchMethodError: 'java.util.concurrent.ExecutorService io.vertx.core.impl.VertxInternal.getWorkerPool()'

last handler in the pipeline did not handle the exception.: io.netty.channel.ChannelPipelineException: io.vertx.core.net.impl.VertxHandler.handlerAdded() has thrown an exception; removed.
2023-07-20T19:54:04.958126142Z  at io.netty.channel.DefaultChannelPipeline.callHandlerAdded0(DefaultChannelPipeline.java:624)
2023-07-20T19:54:04.958134097Z  at io.netty.channel.DefaultChannelPipeline.addLast(DefaultChannelPipeline.java:223)
2023-07-20T19:54:04.958138785Z  at io.netty.channel.DefaultChannelPipeline.addLast(DefaultChannelPipeline.java:195)
2023-07-20T19:54:04.958142132Z  at io.vertx.core.net.impl.NetClientImpl.connected(NetClientImpl.java:313)
2023-07-20T19:54:04.958145217Z  at io.vertx.core.net.impl.NetClientImpl.lambda$connectInternal2$2(NetClientImpl.java:271)
2023-07-20T19:54:04.958148243Z  at io.vertx.core.impl.ContextInternal.dispatch(ContextInternal.java:264)
2023-07-20T19:54:04.958151119Z  at io.vertx.core.net.impl.ChannelProvider.connected(ChannelProvider.java:175)
2023-07-20T19:54:04.958154044Z  at io.vertx.core.net.impl.ChannelProvider.lambda$handleConnect$0(ChannelProvider.java:158)
2023-07-20T19:54:04.958156949Z  at io.netty.util.concurrent.DefaultPromise.notifyListener0(DefaultPromise.java:590)
2023-07-20T19:54:04.958159885Z  at io.netty.util.concurrent.DefaultPromise.notifyListeners0(DefaultPromise.java:583)
2023-07-20T19:54:04.958162790Z  at io.netty.util.concurrent.DefaultPromise.notifyListenersNow(DefaultPromise.java:559)
2023-07-20T19:54:04.958165836Z  at io.netty.util.concurrent.DefaultPromise.notifyListeners(DefaultPromise.java:492)
2023-07-20T19:54:04.958168772Z  at io.netty.util.concurrent.DefaultPromise.setValue0(DefaultPromise.java:636)
2023-07-20T19:54:04.958171657Z  at io.netty.util.concurrent.DefaultPromise.setSuccess0(DefaultPromise.java:625)
2023-07-20T19:54:04.958174653Z  at io.netty.util.concurrent.DefaultPromise.trySuccess(DefaultPromise.java:105)
2023-07-20T19:54:04.958177588Z  at io.netty.channel.DefaultChannelPromise.trySuccess(DefaultChannelPromise.java:84)
2023-07-20T19:54:04.958180604Z  at io.netty.channel.nio.AbstractNioChannel$AbstractNioUnsafe.fulfillConnectPromise(AbstractNioChannel.java:300)
2023-07-20T19:54:04.958183619Z  at io.netty.channel.nio.AbstractNioChannel$AbstractNioUnsafe.finishConnect(AbstractNioChannel.java:335)
2023-07-20T19:54:04.958199038Z  at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:776)
2023-07-20T19:54:04.958204529Z  at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:724)
2023-07-20T19:54:04.958210690Z  at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:650)
2023-07-20T19:54:04.958216261Z  at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:562)
2023-07-20T19:54:04.958222412Z  at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:997)
2023-07-20T19:54:04.958228433Z  at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
2023-07-20T19:54:04.958234575Z  at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
2023-07-20T19:54:04.958240216Z  at java.base/java.lang.Thread.run(Thread.java:833)
2023-07-20T19:54:04.958244904Z Caused by: java.lang.NoSuchMethodError: 'java.util.concurrent.ExecutorService io.vertx.core.impl.VertxInternal.getWorkerPool()'
2023-07-20T19:54:04.958250525Z  at io.vertx.spi.cluster.hazelcast.impl.SubsOpSerializer.execute(SubsOpSerializer.java:60)
2023-07-20T19:54:04.958255665Z  at io.vertx.spi.cluster.hazelcast.HazelcastClusterManager.addRegistration(HazelcastClusterManager.java:361)
2023-07-20T19:54:04.958261455Z  at io.vertx.core.eventbus.impl.clustered.ClusteredEventBus.onLocalRegistration(ClusteredEventBus.java:143)
2023-07-20T19:54:04.958266745Z  at io.vertx.core.eventbus.impl.EventBusImpl.addRegistration(EventBusImpl.java:262)
2023-07-20T19:54:04.958272015Z  at io.vertx.core.eventbus.impl.HandlerRegistration.register(HandlerRegistration.java:63)
2023-07-20T19:54:04.958277906Z  at io.vertx.core.eventbus.impl.MessageConsumerImpl.handler(MessageConsumerImpl.java:226)
2023-07-20T19:54:04.958284469Z  at io.vertx.core.net.impl.NetSocketImpl.registerEventBusHandler(NetSocketImpl.java:108)
2023-07-20T19:54:04.958290039Z  at io.vertx.core.net.impl.NetClientImpl.lambda$connected$8(NetClientImpl.java:309)
2023-07-20T19:54:04.958295529Z  at io.vertx.core.net.impl.VertxHandler.setConnection(VertxHandler.java:82)
2023-07-20T19:54:04.958301080Z  at io.vertx.core.net.impl.VertxHandler.handlerAdded(VertxHandler.java:88)
2023-07-20T19:54:04.958306209Z  at io.netty.channel.AbstractChannelHandlerContext.callHandlerAdded(AbstractChannelHandlerContext.java:1114)
2023-07-20T19:54:04.958316639Z  at io.netty.channel.DefaultChannelPipeline.callHandlerAdded0(DefaultChannelPipeline.java:609)
2023-07-20T19:54:04.958323031Z  ... 25 more
2023-07-20T19:54:04.958329142Z 

This is more or less my setup:

Dockerfile:

FROM quay.io/keycloak/keycloak:21.1.2 as builder

# Enable health and metrics support
ENV KC_HEALTH_ENABLED=false

# Configure a database vendor
ENV KC_DB=postgres
ENV JDBC_PARAMS="connectTimeout=30000"

WORKDIR /opt/keycloak
COPY --chown=keycloak:keycloak ./extensions/package.module-all.jar /opt/keycloak/providers/package.module-all.jar
RUN /opt/keycloak/bin/kc.sh build

FROM quay.io/keycloak/keycloak:21.1.2
COPY --from=builder /opt/keycloak/ /opt/keycloak/
ENTRYPOINT ["/opt/keycloak/bin/kc.sh"]

build.gradle

plugins {
    id 'java'
    id 'org.jetbrains.kotlin.jvm' version '1.9.0'
    id 'com.github.johnrengelman.shadow' version '8.1.1'
    id 'io.quarkus'
}

version ''

repositories {
    mavenCentral()
    jcenter()
}

test {
    useJUnitPlatform()
}

shadowJar {
    zip64 = true
    destinationDirectory.set(file("$rootDir/../keycloak/extensions"))
}

compileKotlin {
    kotlinOptions {
        sourceCompatibility = JavaVersion.VERSION_11
        targetCompatibility = JavaVersion.VERSION_11
    }
}

compileTestKotlin {
    kotlinOptions {
        jvmTarget = "11"
    }
}

dependencies {
    compileOnly group: 'com.google.auto.service', name: 'auto-service', version: '1.0-rc7'
    compileOnly group: 'org.keycloak', name: 'keycloak-server-spi', version: keycloakVersion
    compileOnly group: 'org.keycloak', name: 'keycloak-core', version: keycloakVersion
    compileOnly group: 'org.keycloak', name: 'keycloak-services', version: keycloakVersion
    compileOnly group: 'org.keycloak', name: 'keycloak-server-spi-private', version: keycloakVersion
    compileOnly group: 'org.keycloak', name: 'keycloak-model-jpa', version: keycloakVersion
    compileOnly group: 'io.vertx', name: 'vertx-core', version: vertxVersion
    compileOnly group: 'io.vertx', name: 'vertx-auth-common', version: vertxVersion
    compileOnly group: 'io.vertx', name: 'vertx-bridge-common', version: vertxVersion
    compileOnly group: 'io.vertx', name: 'vertx-codegen', version: vertxVersion
    compileOnly group: 'io.vertx', name: 'vertx-web', version: vertxVersion
    compileOnly group: 'io.vertx', name: 'vertx-web-common', version: vertxVersion
    compileOnly group: 'org.jetbrains.kotlin', name: 'kotlin-stdlib-jdk8', version: '1.7.10'
    compileOnly group: 'org.jetbrains.kotlinx', name: 'kotlinx-coroutines-core', version: '1.6.4'

    compileOnly enforcedPlatform("${quarkusPlatformGroupId}:${quarkusPlatformArtifactId}:${quarkusPlatformVersion}")
    compileOnly group: 'io.quarkus', name: 'quarkus-vertx'
    compileOnly group: 'io.quarkus', name: 'quarkus-arc'

    implementation group: 'io.vertx', name: 'vertx-hazelcast', version: vertxVersion, {
        exclude(group: 'io.vertx')
    }
    implementation group: 'io.vertx', name: 'vertx-lang-kotlin', version: vertxVersion, {
        exclude(group: 'io.vertx')
    }
    implementation group: 'io.vertx', name: 'vertx-lang-kotlin-coroutines', version: vertxVersion, {
        exclude(group: 'io.vertx')
    }
    implementation group: 'io.vertx', name: 'vertx-config', version: vertxVersion, {
        exclude(group: 'io.vertx')
    }
}

EventBusProviderImpl

@AutoService(EventBusProvider::class)
class EventBusProviderImpl : EventBusProvider {
    override fun get(): EventBus {
        if (instance != null) {
            return instance as EventBus
        }

        setupEventBus()
        return instance!!
    }

    companion object {
        private var instance: EventBus? = null

        private fun setupEventBus() {
            runBlocking {
                val container = Arc.container()
                val vertxInjectable = container.select(Vertx::class.java)
                val vertx = vertxInjectable.get()
                vertx.eventBus().publish("test", "{\"test\":\"boo\"}")
                instance = vertx.eventBus()
            }
        }
    }

}

Any idea's or pointers would be more than welcome as the whole Quarkus thing is a tad alien to me compared to the previous JBoss based way of working.


Solution

  • After a lot more fighting with it today, I finally got it to work. The trick is to ensure that you rename just enough packages through the shadowJar relocate method to fool Quarkus in accepting the bundled VertX while not breaking Hazelcast who wants to find the exact same named class when sending events back and forth across the cluster.

    As such this is the setup I settled on in the end:

    build.gradle

    plugins {
        id 'java'
        id 'org.jetbrains.kotlin.jvm' version '1.9.0'
        id 'com.github.johnrengelman.shadow' version '8.1.1'
    }
    
    version ''
    
    repositories {
        mavenCentral()
        jcenter()
    }
    
    test {
        useJUnitPlatform()
    }
    
    shadowJar {
        zip64 = true
        destinationDirectory.set(file("$rootDir/../keycloak/extensions"))
        mergeServiceFiles()
    
        relocate 'io.vertx.core', 'nl.custom.module.bundled.io.vertx.core'
        relocate 'io.vertx.web-client', 'nl.custom.module.bundled.io.vertx.web-client'
        relocate 'io.vertx.lang-kotlin', 'nl.custom.module.bundled.io.vertx.lang-kotlin'
        relocate 'io.vertx.lang-kotlin-coroutines', 'nl.custom.module.bundled.io.vertx.lang-kotlin-coroutines'
        relocate 'io.vertx.config', 'nl.custom.module.bundled.io.vertx.config'
        relocate 'io.vertx.kotlin', 'nl.custom.module.bundled.io.vertx.kotlin'
        relocate 'io.vertx.ext', 'nl.custom.module.bundled.io.vertx.ext'
    }
    
    compileKotlin {
        kotlinOptions {
            sourceCompatibility = JavaVersion.VERSION_11
            targetCompatibility = JavaVersion.VERSION_11
        }
    }
    
    compileTestKotlin {
        kotlinOptions {
            jvmTarget = "11"
        }
    }
    
    assemble.dependsOn shadowJar
    
    dependencies {
        compileOnly group: 'com.google.auto.service', name: 'auto-service', version: '1.0-rc7'
        compileOnly group: 'org.keycloak', name: 'keycloak-server-spi', version: keycloakVersion
        compileOnly group: 'org.keycloak', name: 'keycloak-core', version: keycloakVersion
        compileOnly group: 'org.keycloak', name: 'keycloak-services', version: keycloakVersion
        compileOnly group: 'org.keycloak', name: 'keycloak-server-spi-private', version: keycloakVersion
        compileOnly group: 'org.keycloak', name: 'keycloak-model-jpa', version: keycloakVersion
    
        compileOnly group: 'org.jetbrains.kotlin', name: 'kotlin-stdlib-jdk8', version: '1.7.10'
        compileOnly group: 'org.jetbrains.kotlinx', name: 'kotlinx-coroutines-core', version: '1.6.4'
    
        implementation group: 'io.vertx', name: 'vertx-core', version: vertxVersion
        implementation group: 'io.vertx', name: 'vertx-web-client', version: vertxVersion
        implementation group: 'io.vertx', name: 'vertx-hazelcast', version: vertxVersion
        implementation group: 'io.vertx', name: 'vertx-lang-kotlin', version: vertxVersion
        implementation group: 'io.vertx', name: 'vertx-lang-kotlin-coroutines', version: vertxVersion
        implementation group: 'io.vertx', name: 'vertx-config', version: vertxVersion
    
        testRuntimeOnly group:'org.junit.jupiter', name:'junit-jupiter-engine', version:'5.7.0'
        testImplementation group: 'org.junit.jupiter', name:'junit-jupiter-api', version:'5.7.0'
        testImplementation group: 'io.vertx', name: 'vertx-junit5', version: vertxVersion
    }
    

    After discovering the relocation feature of shadowJar it fell into place quite quickly. Even though the errors about the mismatching classes where very tedious to debug.