temporalio / temporalio/sdk-java

Activity interface which extends an interface with some method using generic types always throws a ClassCastException when said method is called in a Workflow

Open
#1,050 0 comments 1 reaction 0 assignees View on GitHub

Nobody has claimed this yet.

bug
Dominant language
Java
Stars
433
Forks
249
Avg merge
5d 6h
Merged PRs (30d)
26

Description

Expected Behavior

I would expect that an activity interface that extends some other interface with generic types and a method that uses those generic types would have that inherited method be executable as an activity method within a workflow.

Actual Behavior

A method that's inherited and uses generics in an activity interface always throws a ClassCastException when called within a workflow.

An example of the exception (using the repro code included in this issue) is. as follows:

java.lang.ClassCastException: class java.util.LinkedHashMap cannot be cast to class issue.repro.Foo (java.util.LinkedHashMap is in module java.base of loader 'bootstrap'; issue.repro.Foo is in unnamed module of loader 'app')
	at issue.repro.MyActivityImpl.run(ActivityIssueReproTest.kt:38)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:566)
	at io.temporal.internal.activity.POJOActivityTaskHandler$POJOActivityInboundCallsInterceptor.execute(POJOActivityTaskHandler.java:214)
	at io.temporal.internal.activity.POJOActivityTaskHandler$POJOActivityImplementation.execute(POJOActivityTaskHandler.java:180)
	at io.temporal.internal.activity.POJOActivityTaskHandler.handle(POJOActivityTaskHandler.java:120)
	at io.temporal.internal.worker.LocalActivityWorker$TaskHandlerImpl.handleLocalActivity(LocalActivityWorker.java:235)
	at io.temporal.internal.worker.LocalActivityWorker$TaskHandlerImpl.handle(LocalActivityWorker.java:207)
	at io.temporal.internal.worker.LocalActivityWorker$TaskHandlerImpl.handle(LocalActivityWorker.java:196)
	at io.temporal.internal.worker.PollTaskExecutor.lambda$process$0(PollTaskExecutor.java:93)
	at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
	at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
	at java.base/java.lang.Thread.run(Thread.java:829)

Steps to Reproduce the Problem

The repro Kotlin code below uses the Temporal Java SDK (with the Kotlin library), and mockk and Kotest.

import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.registerKotlinModule
import io.kotest.core.spec.style.ShouldSpec
import io.kotest.matchers.shouldBe
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import io.temporal.activity.ActivityInterface
import io.temporal.activity.LocalActivityOptions
import io.temporal.client.WorkflowClientOptions
import io.temporal.client.WorkflowOptions
import io.temporal.common.converter.DefaultDataConverter
import io.temporal.common.converter.JacksonJsonPayloadConverter
import io.temporal.internal.logging.LoggerTag
import io.temporal.testing.TestEnvironmentOptions
import io.temporal.testing.TestWorkflowEnvironment
import io.temporal.worker.Worker
import io.temporal.worker.WorkerOptions
import io.temporal.worker.WorkflowImplementationOptions
import io.temporal.workflow.Workflow
import io.temporal.workflow.WorkflowInterface
import io.temporal.workflow.WorkflowMethod
import java.time.Duration

class Foo

class Bar

interface InterfaceWithGenerics<InputT : Any, OutputT : Any> {
    fun run(input: InputT): OutputT
}

@ActivityInterface
interface MyActivity : InterfaceWithGenerics<Foo, Bar>

class MyActivityImpl : MyActivity {
    override fun run(input: Foo): Bar {
        return Bar()
    }
}

@WorkflowInterface
interface MyWorkflow {
    @WorkflowMethod
    fun run(input: Foo): Bar
}

class MyWorkflowImpl : MyWorkflow {
    private val myActivity = Workflow.newLocalActivityStub(
        MyActivity::class.java,
        LocalActivityOptions { setStartToCloseTimeout(Duration.ofSeconds(10)) }
    )

    override fun run(input: Foo): Bar {
        return myActivity.run(input)
    }
}

class ActivityIssueReproTest : ShouldSpec({
    val myActivity = mockk<MyActivityImpl>()

    val testEnv: TestWorkflowEnvironment = run {
        val objectMapper: ObjectMapper = ObjectMapper().also { mapper ->
            mapper.registerKotlinModule()
        }
        val dataConverter = DefaultDataConverter(JacksonJsonPayloadConverter(objectMapper))
        val workflowClientOptions = WorkflowClientOptions.newBuilder()
            .setDataConverter(dataConverter)
            .build()
        val workflowTestEnvironmentOptions = TestEnvironmentOptions.newBuilder()
            .setWorkflowClientOptions(workflowClientOptions)
            .build()

        TestWorkflowEnvironment.newInstance(workflowTestEnvironmentOptions)
    }

    val worker: Worker = run {
        val workerOptions: WorkerOptions = WorkerOptions.newBuilder()
            .setDefaultDeadlockDetectionTimeout(5_000L)
            .validateAndBuildWithDefaults()

        testEnv.newWorker(LoggerTag.TASK_QUEUE, workerOptions).also { worker ->
            // Register the workflow implementation(s)
            worker.registerWorkflowImplementationTypes(
                WorkflowImplementationOptions { setFailWorkflowExceptionTypes(Throwable::class.java) },
                MyWorkflowImpl::class.java
            )
            // Register the activity implementation(s)
            worker.registerActivitiesImplementations(myActivity)
        }
    }

    val defaultWorkflowOptions = WorkflowOptions
        .newBuilder()
        .setTaskQueue(LoggerTag.TASK_QUEUE)
        .build()

    beforeTest {
        testEnv.start()
    }

    should("repro the Temporal issue we're seeing") {
        val response = Bar()
        every { myActivity.run(any()) } returns response

        val myWorkflow = testEnv.workflowClient.newWorkflowStub(
            MyWorkflow::class.java,
            defaultWorkflowOptions
        )

        val foo = Foo()
        val result = myWorkflow.run(foo)

        result shouldBe response
        verify {
            myActivity.run(foo)
        }
    }

    afterTest {
        testEnv.close()
    }
})

Specifications

  • Version: 1.8.0
  • Platform: macOS

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the POJOActivityTaskHandler stack frames and the Kotlin ActivityIssueReproTest example. Trace how the inherited generic run method is discovered and how its argument is decoded, then verify the repro no longer casts a LinkedHashMap to Foo and the activity completes successfully.

Written by the indexing model from the issue text.

Assessment

Tech stack
java, kotlin
Domain
backend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
38/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.