[
https://issues.apache.org/jira/browse/GROOVY-12163?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18096004#comment-18096004
]
ASF GitHub Bot commented on GROOVY-12163:
-----------------------------------------
Copilot commented on code in PR #2708:
URL: https://github.com/apache/groovy/pull/2708#discussion_r3574431031
##########
src/test/groovy/groovy/lang/ExpandoMetaClassMixinConcurrencyTest.groovy:
##########
@@ -0,0 +1,108 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package groovy.lang
+
+import org.codehaus.groovy.reflection.MixinInMetaClass
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.Timeout
+
+import java.util.concurrent.CopyOnWriteArrayList
+import java.util.concurrent.CyclicBarrier
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.atomic.AtomicBoolean
+
+import static org.junit.jupiter.api.Assertions.assertNotNull
+import static org.junit.jupiter.api.Assertions.assertTrue
+
+final class ExpandoMetaClassMixinConcurrencyTest {
+
+ private static final int CATEGORY_COUNT = 40
+ private static final int PRE_MIXED_COUNT = 8
+ private static final int READER_COUNT = 3
+ private static final int TIMEOUT_SECONDS = 30
+
+ static class Target {}
+
+ // Adding a mixin (addMixinClass) mutates the backing mixin set while
method
+ // dispatch iterates it (findMixinMethod). The set must tolerate that; a
plain
+ // LinkedHashSet threw ConcurrentModificationException in the dispatching
thread.
+ @Test
+ @Timeout(60)
+ void mixinAddIsSafeAgainstConcurrentDispatch() {
+ def loader = new GroovyClassLoader()
+ // distinct category classes so every mixin is a fresh structural add
+ def categories = (0..<CATEGORY_COUNT).collect { i ->
+ loader.parseClass("class Cat_$i { def catMethod_$i() { 'x' } }",
"Cat_${i}.groovy")
+ }
+
+ def emc = new ExpandoMetaClass(Target, false, true)
+ emc.initialize()
+
+ // pre-populate so the readers always have something to iterate over
+ categories[0..<PRE_MIXED_COUNT].each {
MixinInMetaClass.mixinClassesToMetaClass(emc, [it]) }
+
+ def errors = new CopyOnWriteArrayList<Throwable>()
+ def stop = new AtomicBoolean(false)
+ def noArgs = new Class[0]
+ def barrier = new CyclicBarrier(READER_COUNT + 1)
+
+ def readers = (1..READER_COUNT).collect {
+ Thread.start {
+ try {
+ barrier.await(TIMEOUT_SECONDS, TimeUnit.SECONDS)
+ while (!stop.get()) {
+ emc.findMixinMethod('noSuchMixinMethod', noArgs)
+ }
Review Comment:
The reader loop ignores thread interruption. If the test detects stuck
threads and interrupts them, the readers will keep spinning until `stop`
becomes true, which can leak threads when something goes wrong before `stop` is
set (e.g. writer hang). Consider also terminating the loop when the thread is
interrupted.
##########
src/test/groovy/groovy/lang/ExpandoMetaClassMixinConcurrencyTest.groovy:
##########
@@ -0,0 +1,108 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package groovy.lang
+
+import org.codehaus.groovy.reflection.MixinInMetaClass
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.Timeout
+
+import java.util.concurrent.CopyOnWriteArrayList
+import java.util.concurrent.CyclicBarrier
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.atomic.AtomicBoolean
+
+import static org.junit.jupiter.api.Assertions.assertNotNull
+import static org.junit.jupiter.api.Assertions.assertTrue
+
+final class ExpandoMetaClassMixinConcurrencyTest {
+
+ private static final int CATEGORY_COUNT = 40
+ private static final int PRE_MIXED_COUNT = 8
+ private static final int READER_COUNT = 3
+ private static final int TIMEOUT_SECONDS = 30
+
+ static class Target {}
+
+ // Adding a mixin (addMixinClass) mutates the backing mixin set while
method
+ // dispatch iterates it (findMixinMethod). The set must tolerate that; a
plain
+ // LinkedHashSet threw ConcurrentModificationException in the dispatching
thread.
+ @Test
+ @Timeout(60)
+ void mixinAddIsSafeAgainstConcurrentDispatch() {
+ def loader = new GroovyClassLoader()
+ // distinct category classes so every mixin is a fresh structural add
+ def categories = (0..<CATEGORY_COUNT).collect { i ->
+ loader.parseClass("class Cat_$i { def catMethod_$i() { 'x' } }",
"Cat_${i}.groovy")
+ }
+
+ def emc = new ExpandoMetaClass(Target, false, true)
+ emc.initialize()
+
+ // pre-populate so the readers always have something to iterate over
+ categories[0..<PRE_MIXED_COUNT].each {
MixinInMetaClass.mixinClassesToMetaClass(emc, [it]) }
+
+ def errors = new CopyOnWriteArrayList<Throwable>()
+ def stop = new AtomicBoolean(false)
+ def noArgs = new Class[0]
+ def barrier = new CyclicBarrier(READER_COUNT + 1)
+
+ def readers = (1..READER_COUNT).collect {
+ Thread.start {
+ try {
+ barrier.await(TIMEOUT_SECONDS, TimeUnit.SECONDS)
+ while (!stop.get()) {
+ emc.findMixinMethod('noSuchMixinMethod', noArgs)
+ }
+ } catch (Throwable t) {
+ errors << t
+ } finally {
+ stop.set(true)
+ }
+ }
+ }
+ def writer = Thread.start {
+ try {
+ barrier.await(TIMEOUT_SECONDS, TimeUnit.SECONDS)
+ categories[PRE_MIXED_COUNT..<CATEGORY_COUNT].each {
+ MixinInMetaClass.mixinClassesToMetaClass(emc, [it])
+ }
+ } catch (Throwable t) {
+ errors << t
+ } finally {
+ stop.set(true)
+ }
+ }
+
+ def threads = [writer, *readers]
+ threads.each { it.join(TimeUnit.SECONDS.toMillis(TIMEOUT_SECONDS)) }
+ def stuck = threads.findAll { it.alive }
+ stuck*.interrupt()
+ assertTrue(stuck.isEmpty(), "threads still running after
${TIMEOUT_SECONDS}s: $stuck")
Review Comment:
When threads are still alive after the join timeout, the test interrupts
them but doesn't set `stop` first. Since the reader loop doesn't inherently
respond to interrupts, setting `stop` here improves cleanup and reduces the
risk of leaking threads into the rest of the test suite when this test fails.
> fix concurrent modification of ExpandoMetaClass mixinClasses
> ------------------------------------------------------------
>
> Key: GROOVY-12163
> URL: https://issues.apache.org/jira/browse/GROOVY-12163
> Project: Groovy
> Issue Type: Bug
> Reporter: Paul King
> Priority: Major
>
> see: https://github.com/apache/groovy/pull/2698
--
This message was sent by Atlassian Jira
(v8.20.10#820010)