Skip to content

Commit 1c06aad

Browse files
committed
[Driver/Java] Fix a race condition when there are only spies and spies simulate connection, i.e. adding/removing spies mutated the array that the sender thread was iterating over. Now conductor computes max spy position while processing stream counters and share it via a volatile long field.
(cherry picked from commit 1dde9e3)
1 parent f3e11d6 commit 1c06aad

3 files changed

Lines changed: 113 additions & 23 deletions

File tree

aeron-driver/src/main/java/io/aeron/driver/NetworkPublication.java

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@
3838
import org.agrona.concurrent.status.Position;
3939
import org.agrona.concurrent.status.ReadablePosition;
4040

41+
import java.lang.invoke.MethodHandles;
42+
import java.lang.invoke.VarHandle;
4143
import java.net.InetSocketAddress;
4244
import java.nio.ByteBuffer;
4345
import java.util.ArrayList;
@@ -79,12 +81,27 @@ class NetworkPublicationPadding1
7981
class NetworkPublicationConductorFields extends NetworkPublicationPadding1
8082
{
8183
static final ReadablePosition[] EMPTY_POSITIONS = new ReadablePosition[0];
84+
static final VarHandle MAX_SPY_POSITION_MH;
85+
86+
static
87+
{
88+
try
89+
{
90+
MAX_SPY_POSITION_MH = MethodHandles.lookup()
91+
.findVarHandle(NetworkPublicationConductorFields.class, "maxSpyPosition", long.class);
92+
}
93+
catch (final ReflectiveOperationException ex)
94+
{
95+
throw new ExceptionInInitializerError(ex);
96+
}
97+
}
8298

8399
long cleanPosition;
84100
long timeOfLastActivityNs;
85101
long lastSenderPosition;
86102
int refCount = 0;
87103
ReadablePosition[] spyPositions = EMPTY_POSITIONS;
104+
volatile long maxSpyPosition;
88105
final ArrayList<UntetheredSubscription> untetheredSubscriptions = new ArrayList<>();
89106
}
90107

@@ -677,7 +694,7 @@ int send(final long nowNs)
677694

678695
if (spiesSimulateConnection && hasSpies && !hasReceivers)
679696
{
680-
final long newSenderPosition = maxSpyPosition(senderPosition);
697+
final long newSenderPosition = Math.max((long)MAX_SPY_POSITION_MH.getAcquire(this), senderPosition);
681698
this.senderPosition.setRelease(newSenderPosition);
682699
senderLimit.setRelease(flowControl.onIdle(nowNs, newSenderPosition, newSenderPosition, isEndOfStream));
683700
}
@@ -779,10 +796,14 @@ int updatePublisherPositionAndLimit()
779796
if (hasSubscribers())
780797
{
781798
long minConsumerPosition = senderPosition;
799+
long maxConsumerPosition = senderPosition;
782800
for (final ReadablePosition spyPosition : spyPositions)
783801
{
784-
minConsumerPosition = Math.min(minConsumerPosition, spyPosition.getVolatile());
802+
final long position = spyPosition.getAcquire();
803+
minConsumerPosition = Math.min(minConsumerPosition, position);
804+
maxConsumerPosition = Math.max(maxConsumerPosition, position);
785805
}
806+
MAX_SPY_POSITION_MH.setRelease(this, maxConsumerPosition);
786807

787808
final long newLimitPosition = minConsumerPosition + termWindowLength;
788809
if (newLimitPosition > publisherLimit.get())
@@ -1029,18 +1050,6 @@ private boolean spiesFinishedConsuming(final DriverConductor conductor, final lo
10291050
return true;
10301051
}
10311052

1032-
private long maxSpyPosition(final long senderPosition)
1033-
{
1034-
long position = senderPosition;
1035-
1036-
for (final ReadablePosition spyPosition : spyPositions)
1037-
{
1038-
position = Math.max(position, spyPosition.getVolatile());
1039-
}
1040-
1041-
return position;
1042-
}
1043-
10441053
private void updateConnectedState(final boolean newConnectedState)
10451054
{
10461055
if (newConnectedState != isConnected)

aeron-system-tests/src/test/java/io/aeron/SpySimulatedConnectionTest.java

Lines changed: 71 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import io.aeron.test.driver.TestMediaDriver;
3030
import org.agrona.CloseHelper;
3131
import org.agrona.collections.MutableInteger;
32+
import org.agrona.concurrent.NoOpIdleStrategy;
3233
import org.agrona.concurrent.UnsafeBuffer;
3334
import org.agrona.concurrent.status.CountersReader;
3435
import org.junit.jupiter.api.AfterEach;
@@ -85,14 +86,17 @@ private static List<String> channels()
8586
@RegisterExtension
8687
final SystemTestWatcher watcher = new SystemTestWatcher();
8788

88-
private void launch()
89+
private void launch(final ThreadingMode threadingMode)
8990
{
9091
driverContext
9192
.aeronDirectoryName(CommonContext.generateRandomDirName())
9293
.publicationTermBufferLength(TERM_BUFFER_LENGTH)
9394
.errorHandler(Tests::onError)
9495
.dirDeleteOnStart(true)
95-
.threadingMode(ThreadingMode.SHARED);
96+
.threadingMode(threadingMode)
97+
.senderIdleStrategy(NoOpIdleStrategy.INSTANCE)
98+
.receiverIdleStrategy(NoOpIdleStrategy.INSTANCE)
99+
.conductorIdleStrategy(NoOpIdleStrategy.INSTANCE);
96100

97101
driver = TestMediaDriver.launch(driverContext, watcher);
98102
watcher.dataCollector().add(driver.context().aeronDirectory());
@@ -111,7 +115,7 @@ void after()
111115
@InterruptAfter(10)
112116
void shouldNotSimulateConnectionWhenNotConfigured(final String channel)
113117
{
114-
launch();
118+
launch(ThreadingMode.SHARED);
115119

116120
spy = client.addSubscription(spyForChannel(channel), STREAM_ID);
117121
publication = client.addPublication(channel, STREAM_ID);
@@ -126,7 +130,7 @@ void shouldNotSimulateConnectionWhenNotConfigured(final String channel)
126130
@InterruptAfter(10)
127131
void shouldSimulateConnectionWhenOnChannel(final String channel)
128132
{
129-
launch();
133+
launch(ThreadingMode.SHARED);
130134

131135
spy = client.addSubscription(spyForChannel(channel), STREAM_ID);
132136
publication = client.addPublication(channel + "|ssc=true", STREAM_ID);
@@ -147,7 +151,7 @@ void shouldSimulateConnectionWithNoNetworkSubscriptions(final String channel)
147151
.timerIntervalNs(TimeUnit.MILLISECONDS.toNanos(100))
148152
.spiesSimulateConnection(true);
149153

150-
launch();
154+
launch(ThreadingMode.SHARED);
151155

152156
spy = client.addSubscription(spyForChannel(channel), STREAM_ID);
153157
publication = client.addPublication(channel, STREAM_ID);
@@ -198,7 +202,7 @@ void shouldSimulateConnectionWithSlowNetworkSubscription(final String channel)
198202
.timerIntervalNs(TimeUnit.MILLISECONDS.toNanos(100))
199203
.spiesSimulateConnection(true);
200204

201-
launch();
205+
launch(ThreadingMode.SHARED);
202206

203207
spy = client.addSubscription(spyForChannel(channel), STREAM_ID);
204208
subscription = client.addSubscription(channel, STREAM_ID);
@@ -247,7 +251,7 @@ void shouldSimulateConnectionWithLeavingNetworkSubscription(final String channel
247251
.timerIntervalNs(TimeUnit.MILLISECONDS.toNanos(100))
248252
.spiesSimulateConnection(true);
249253

250-
launch();
254+
launch(ThreadingMode.SHARED);
251255

252256
spy = client.addSubscription(spyForChannel(channel), STREAM_ID);
253257
subscription = client.addSubscription(channel, STREAM_ID);
@@ -290,7 +294,7 @@ else if (!isSubscriptionClosed)
290294
@InterruptAfter(10)
291295
void shouldNotHaveShortSendsWithMDCPublication()
292296
{
293-
launch();
297+
launch(ThreadingMode.SHARED);
294298

295299
final String channel = "aeron:udp?control=localhost:20000|control-mode=dynamic";
296300

@@ -323,7 +327,7 @@ void shouldNotHaveShortSendsWithMDCPublication()
323327
void shouldNotChangeConnectionStatusOnPublicationIfNormalSubscriberGoesAway(final String channel)
324328
throws InterruptedException
325329
{
326-
launch();
330+
launch(ThreadingMode.SHARED);
327331

328332
spy = client.addSubscription(spyForChannel(channel), STREAM_ID);
329333
subscription = client.addSubscription(channel, STREAM_ID);
@@ -377,6 +381,64 @@ void shouldNotChangeConnectionStatusOnPublicationIfNormalSubscriberGoesAway(fina
377381
assertTrue(spy.isConnected());
378382
}
379383

384+
@ParameterizedTest
385+
@MethodSource("channels")
386+
@InterruptAfter(10)
387+
void addingAndRemovingSpiesIsThreadSafe(final String channel) throws InterruptedException
388+
{
389+
launch(ThreadingMode.DEDICATED);
390+
391+
final String spyChannel = spyForChannel(channel);
392+
spy = client.addSubscription(spyChannel, STREAM_ID);
393+
publication = client.addPublication(channel + "|ssc=true", STREAM_ID);
394+
Tests.awaitConnected(publication);
395+
Tests.awaitConnected(spy);
396+
397+
final AtomicBoolean running = new AtomicBoolean(true);
398+
final Thread sender = new Thread(() ->
399+
{
400+
while (running.get())
401+
{
402+
if (publication.offer(buffer) < 0)
403+
{
404+
Tests.yield();
405+
}
406+
}
407+
});
408+
sender.start();
409+
410+
final long[] spyRegistrationIds = new long[100];
411+
for (int i = 0; i < spyRegistrationIds.length; i++)
412+
{
413+
spyRegistrationIds[i] = client.asyncAddSubscription(spyChannel, STREAM_ID);
414+
}
415+
416+
final Subscription[] spies = new Subscription[spyRegistrationIds.length];
417+
418+
int count;
419+
do
420+
{
421+
count = 0;
422+
for (int i = 0; i < spyRegistrationIds.length; i++)
423+
{
424+
if (null == spies[i])
425+
{
426+
spies[i] = client.getSubscription(spyRegistrationIds[i]);
427+
}
428+
else
429+
{
430+
count++;
431+
}
432+
}
433+
}
434+
while (spyRegistrationIds.length != count);
435+
436+
CloseHelper.closeAll(spies);
437+
438+
running.set(false);
439+
sender.join();
440+
}
441+
380442
private void waitUntilFullConnectivity()
381443
{
382444
while (!spy.isConnected() || !subscription.isConnected() || !publication.isConnected())

aeron-test-support/src/main/java/io/aeron/test/driver/CTestMediaDriver.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import org.agrona.collections.MutableInteger;
2929
import org.agrona.collections.Object2ObjectHashMap;
3030
import org.agrona.concurrent.AgentInvoker;
31+
import org.agrona.concurrent.IdleStrategy;
3132
import org.agrona.concurrent.status.CountersReader;
3233

3334
import java.io.File;
@@ -36,6 +37,7 @@
3637
import java.nio.file.Files;
3738
import java.util.*;
3839
import java.util.concurrent.TimeUnit;
40+
import java.util.function.Supplier;
3941

4042
public final class CTestMediaDriver implements TestMediaDriver
4143
{
@@ -239,6 +241,11 @@ public static CTestMediaDriver launch(
239241
{
240242
environment.put("AERON_THREADING_MODE", context.threadingMode().name());
241243
}
244+
setIdleStrategy(environment, "AERON_CONDUCTOR_IDLE_STRATEGY", context::conductorIdleStrategy);
245+
setIdleStrategy(environment, "AERON_SENDER_IDLE_STRATEGY", context::senderIdleStrategy);
246+
setIdleStrategy(environment, "AERON_RECEIVER_IDLE_STRATEGY", context::receiverIdleStrategy);
247+
setIdleStrategy(environment, "AERON_SHARED_IDLE_STRATEGY", context::sharedIdleStrategy);
248+
setIdleStrategy(environment, "AERON_SHAREDNETWORK_IDLE_STRATEGY", context::sharedNetworkIdleStrategy);
242249
environment.put("AERON_TIMER_INTERVAL", String.valueOf(context.timerIntervalNs()));
243250
environment.put(
244251
"AERON_UNTETHERED_WINDOW_LIMIT_TIMEOUT", String.valueOf(context.untetheredWindowLimitTimeoutNs()));
@@ -345,6 +352,18 @@ public static CTestMediaDriver launch(
345352
}
346353
}
347354

355+
private static void setIdleStrategy(
356+
final HashMap<String, String> environment,
357+
final String envKey,
358+
final Supplier<IdleStrategy> idleStrategySupplier)
359+
{
360+
final IdleStrategy idleStrategy = idleStrategySupplier.get();
361+
if (null != idleStrategy)
362+
{
363+
environment.put(envKey, idleStrategy.alias());
364+
}
365+
}
366+
348367
public MediaDriver.Context context()
349368
{
350369
return context;

0 commit comments

Comments
 (0)