Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Declaring the global read-write lock on a leaf node should not fail execution #4027

Closed
fslev opened this issue Sep 26, 2024 · 8 comments · Fixed by #4039
Closed

Declaring the global read-write lock on a leaf node should not fail execution #4027

fslev opened this issue Sep 26, 2024 · 8 comments · Fixed by #4039

Comments

@fslev
Copy link

fslev commented Sep 26, 2024

I've upgraded my Cucumber test framework with JUnit 5.11.1 and I get a new error:

[ERROR]   Task was deferred but should have been executed synchronously: NodeTestTask [PickleDescriptor: [engine:junit-platform-suite]/[suite:com.ionos.cloud.cucumber.ml.MlApiTest]/[engine:cucumber]/[feature:classpath%3Afeatures%2FModel%2FModelLB.feature]/[scenario:26]]

All I can say is that inside the feature file I have two scenarios: one marked with @isolated global read write exclusive resource and another scenario is marked with a simple read write exclusive resource.

Reverting to JUnit 5.11.0 works without any error !

I'm sorry I cannot give more details. Maybe I will try to isolate it to a more detailed scenario when I'll have more time.

@mpkorstanje
Copy link
Contributor

mpkorstanje commented Sep 26, 2024

This can be reproduced with https://github.com/mpkorstanje/junit5-locks/tree/main

Feature: example

  @isolated
  Scenario: a
    When I wait 1 hour

  @reads-and-writes-system-properties
  Scenario: b
    When I wait 1 hour
cucumber.execution.parallel.enabled=true
cucumber.execution.parallel.config.strategy=fixed
cucumber.execution.parallel.config.fixed.parallelism=2
cucumber.execution.exclusive-resources.isolated.read-write=org.junit.platform.engine.support.hierarchical.ExclusiveResource.GLOBAL_KEY
cucumber.execution.exclusive-resources.reads-and-writes-system-properties.read-write=java.lang.System.properties
package io.cucumber.skeleton;

import io.cucumber.java.en.When;

import java.util.concurrent.TimeUnit;

public class StepDefinitions {
    @When("I wait {int} hour")
    public void iWaitHour(int arg0) throws InterruptedException {
        TimeUnit.SECONDS.sleep(arg0);
    }
}

Unlike with JUnit Jupiter the ExclusiveResource.GLOBAL_KEY is not used at top level:

example
 - a  <- locks ExclusiveResource.GLOBAL_KEY
 - b <-  locks java.lang.System.properties

Prior to executing b, the nop lock from example and the exclusive resource from a is still held in the thread locks.

resourceLock = {SingleLock@3016} "SingleLock [resource = ExclusiveResource [key = 'java.lang.System.properties', lockMode = READ_WRITE]]"
 resources = {Collections$SingletonList@3025}  size = 1
  0 = {ExclusiveResource@3029} "ExclusiveResource [key = 'java.lang.System.properties', lockMode = READ_WRITE]"
 lock = {ReentrantReadWriteLock$WriteLock@3026} "java.util.concurrent.locks.ReentrantReadWriteLock$WriteLock@2c79993b[Unlocked]"
threadLock = {ForkJoinPoolHierarchicalTestExecutorService$ThreadLock@2471} 
 locks = {ArrayDeque@3023}  size = 2
  0 = {SingleLock@2677} "SingleLock [resource = ExclusiveResource [key = 'org.junit.platform.engine.support.hierarchical.ExclusiveResource.GLOBAL_KEY', lockMode = READ_WRITE]]"
  1 = {NopLock@2217} "NopLock []"

So then ResourceLock.isCompatible returns false because

boolean isGlobalReadLock = ownResources.size() == 1
&& ExclusiveResource.GLOBAL_READ.equals(ownResources.get(0));
if ((!isGlobalReadLock && other.isExclusive()) || this.isExclusive()) {
return false;
}

And it is worth noting that the comment about the ExclusiveResource.GLOBAL_KEY only applies to JUnit Jupiter.

// The global read lock (which is always on direct children of the engine node)
// needs special treatment so that it is compatible with the first write lock
// (which may be on a test method).

@marcphilipp
Copy link
Member

Does Cucumber somehow bypass NodeTreeWalker which ensures the global lock is acquired at the top level?

@mpkorstanje
Copy link
Contributor

mpkorstanje commented Oct 1, 2024

Cucumber does not. I'd say that the CucumberTestEngine is fairly unremarkable in that regard.

The difference is that JUnits @Isolated can only be applied at class level. And classes are always the children of a test engine. Where as in Cucumber the the global lock is applied at what would be JUnits method level. At a glance this seems to run counter to assumptions with which the NodeTreeWalker was written. I'd have to debug it a bit more to be certain but I haven't got a long enough stretch of time available for that right now.

Note: It doesn't look like any actual parallelism is needed to reproduce the problem. This will also produce the problem:

cucumber.execution.parallel.config.fixed.parallelism=1
cucumber.execution.parallel.config.fixed.max-pool-size=1

@mpkorstanje
Copy link
Contributor

mpkorstanje commented Oct 1, 2024

With some trickery it is possible effectively put @Isolated on a method. This results in the error message as with Cucumber.

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.parallel.ResourceLock;

import java.util.concurrent.TimeUnit;

import static org.junit.jupiter.api.parallel.ResourceAccessMode.READ_WRITE;
import static org.junit.platform.engine.support.hierarchical.ExclusiveResource.GLOBAL_KEY;

public class LockTest {

  @Test
  @ResourceLock(value = GLOBAL_KEY, mode = READ_WRITE) // effectively @Isolated
  void test1() throws InterruptedException {
    TimeUnit.SECONDS.sleep(1);
  }

  @Test
  @ResourceLock(value = "b", mode = READ_WRITE)
  void test2() throws InterruptedException {
    TimeUnit.SECONDS.sleep(1);
  }

}
junit.jupiter.execution.parallel.mode.default=concurrent
junit.jupiter.execution.parallel.enabled=true
junit.jupiter.execution.parallel.config.strategy=fixed
junit.jupiter.execution.parallel.config.fixed.parallelism=1

@marcphilipp
Copy link
Member

Thanks for the reproducer! I'll take a look today.

@marcphilipp marcphilipp added this to the 5.11.2 milestone Oct 2, 2024
marcphilipp added a commit that referenced this issue Oct 2, 2024
Prior to this change additional locks were not cleared from siblings
when discovering the global read-write lock on a test descriptor which
led to incompatible locks and caused test execution to fail.

Fixes #4027.
@marcphilipp marcphilipp changed the title [ERROR] Task was deferred but should have been executed synchronously Declaring the global read-write lock on a leaf node should not fail execution Oct 2, 2024
@marcphilipp
Copy link
Member

I've verified that #4039 fixes the problem for the reproducer.

marcphilipp added a commit that referenced this issue Oct 2, 2024
Prior to this change additional locks were not cleared from siblings
when discovering the global read-write lock on a test descriptor which
led to incompatible locks and caused test execution to fail.

Fixes #4027.
@mpkorstanje
Copy link
Contributor

Cheers. That was quick!

marcphilipp added a commit that referenced this issue Oct 3, 2024
Prior to this change additional locks were not cleared from siblings
when discovering the global read-write lock on a test descriptor which
led to incompatible locks and caused test execution to fail.

Fixes #4027.

(cherry picked from commit 871e800)
@marcphilipp
Copy link
Member

Backported to the releases/5.11.x branch ✔️

marcphilipp added a commit that referenced this issue Oct 4, 2024
Prior to this change additional locks were not cleared from siblings
when discovering the global read-write lock on a test descriptor which
led to incompatible locks and caused test execution to fail.

Fixes #4027.

(cherry picked from commit 871e800)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants