Skip to content

Commit 2d12515

Browse files
author
Myron Scott
authored
Merge pull request #1331 from myronkscott/harden_client_ref
Make the server more tolerant of client mistakes during reference/dereference
2 parents 4415c58 + 003ce46 commit 2d12515

File tree

4 files changed

+91
-39
lines changed

4 files changed

+91
-39
lines changed

tc-client/src/main/java/com/tc/object/ClientEntityManagerImpl.java

Lines changed: 29 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,6 @@
5757
import com.tc.text.MapListPrettyPrint;
5858
import com.tc.text.PrettyPrintable;
5959
import com.tc.util.Assert;
60-
import com.tc.util.Util;
6160
import java.io.IOException;
6261
import java.nio.ByteBuffer;
6362

@@ -80,6 +79,7 @@
8079

8180
import static com.tc.object.EntityDescriptor.createDescriptorForLifecycle;
8281
import static com.tc.object.SafeInvocationCallback.safe;
82+
import java.util.concurrent.Callable;
8383
import static java.util.stream.Collectors.toCollection;
8484
import static org.terracotta.entity.Invocation.uninterruptiblyGet;
8585

@@ -409,6 +409,20 @@ private <M extends EntityMessage, R extends EntityResponse> EntityClientEndpoint
409409
Assert.assertNotNull("Can't lookup null entity descriptor", instance);
410410
final EntityDescriptor fetchDescriptor = EntityDescriptor.createDescriptorForFetch(entity, version, instance);
411411
EntityClientEndpointImpl<M, R> resolvedEndpoint = null;
412+
// make sure release is only ever called once
413+
Callable<Void> closeCall = new Callable<Void>() {
414+
boolean released = false;
415+
416+
@Override
417+
public synchronized Void call() throws Exception {
418+
if (!released) {
419+
released = true;
420+
internalRelease(entity, fetchDescriptor);
421+
}
422+
return null;
423+
}
424+
};
425+
412426
try {
413427
byte[] raw = internalRetrieve(fetchDescriptor);
414428
ByteBuffer br = ByteBuffer.wrap(raw);
@@ -418,21 +432,8 @@ private <M extends EntityMessage, R extends EntityResponse> EntityClientEndpoint
418432
br.get(config);
419433
// We can only fail to get the config if we threw an exception.
420434
Assert.assertTrue(null != raw);
421-
// We managed to retrieve the config so create the end-point.
422-
// Note that we will need to call release on this descriptor, when it is closed, prior to running the closeHook we
423-
// were given so combine these ideas to pass in one runnable.
424-
Runnable compoundRunnable = new Runnable() {
425-
@Override
426-
public void run() {
427-
try {
428-
internalRelease(entity, fetchDescriptor);
429-
} catch (EntityException e) {
430-
// We aren't expecting there to be any problems releasing an entity in the close hook so we will just log and re-throw.
431-
Util.printLogAndRethrowError(e, logger);
432-
}
433-
}
434-
};
435-
resolvedEndpoint = new EntityClientEndpointImpl<>(entity, version, EntityDescriptor.createDescriptorForInvoke(fetch, instance), this, config, codec, compoundRunnable, this.endpointCloser);
435+
436+
resolvedEndpoint = new EntityClientEndpointImpl<>(entity, version, EntityDescriptor.createDescriptorForInvoke(fetch, instance), this, config, codec, closeCall, this.endpointCloser);
436437

437438
if (this.objectStoreMap.putIfAbsent(instance, resolvedEndpoint) != null) {
438439
throw Assert.failure("Attempt to add an object that already exists: Object of class " + resolvedEndpoint.getClass()
@@ -442,14 +443,22 @@ public void run() {
442443
throw notfound;
443444
} catch (EntityException e) {
444445
// Release the entity and re-throw to the higher level.
445-
internalRelease(entity, fetchDescriptor);
446-
// NOTE: Since we are throwing, we are not responsible for calling the given closeHook.
446+
try {
447+
closeCall.call();
448+
} catch (Exception subE) {
449+
e.addSuppressed(subE);
450+
}
451+
447452
throw e;
448453
} catch (Throwable t) {
449454
// This is the unexpected case so clean up and re-throw as a RuntimeException
450455
// Clean up any client-side or server-side state regarding this failed connection.
451-
internalRelease(entity, fetchDescriptor);
452-
// NOTE: Since we are throwing, we are not responsible for calling the given closeHook.
456+
try {
457+
closeCall.call();
458+
} catch (Exception subE) {
459+
t.addSuppressed(subE);
460+
}
461+
453462
throw Throwables.propagate(t);
454463
}
455464

tc-client/src/main/java/com/tc/object/EntityClientEndpointImpl.java

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
import org.slf4j.LoggerFactory;
4141

4242
import static com.tc.object.SafeInvocationCallback.safe;
43+
import org.terracotta.exception.EntityException;
4344

4445

4546
public class EntityClientEndpointImpl<M extends EntityMessage, R extends EntityResponse> implements EntityClientEndpoint<M, R> {
@@ -52,7 +53,7 @@ public class EntityClientEndpointImpl<M extends EntityMessage, R extends EntityR
5253
private final EntityID entityID;
5354
private final long version;
5455
private final MessageCodec<M, R> codec;
55-
private final Runnable closeHook;
56+
private final Callable<Void> closeHook;
5657
private final ExecutorService closer;
5758
private EndpointDelegate<R> delegate;
5859
private boolean isOpen;
@@ -66,7 +67,7 @@ public class EntityClientEndpointImpl<M extends EntityMessage, R extends EntityR
6667
* @param entityConfiguration Opaque byte[] describing how to configure the entity to be built on top of this end-point.
6768
* @param closeHook A Runnable which will be run last when the end-point is closed.
6869
*/
69-
public EntityClientEndpointImpl(EntityID eid, long version, EntityDescriptor instance, InvocationHandler invocationHandler, byte[] entityConfiguration, MessageCodec<M, R> codec, Runnable closeHook, ExecutorService closer) {
70+
public EntityClientEndpointImpl(EntityID eid, long version, EntityDescriptor instance, InvocationHandler invocationHandler, byte[] entityConfiguration, MessageCodec<M, R> codec, Callable<Void> closeHook, ExecutorService closer) {
7071
this.entityID = eid;
7172
this.version = version;
7273
this.invokeDescriptor = instance;
@@ -176,12 +177,16 @@ public byte[] getExtendedReconnectData() {
176177
@Override
177178
public void close() {
178179
// We can't close twice.
179-
checkEndpointOpen();
180-
if (this.closeHook != null) {
181-
this.closeHook.run();
180+
if (closeIfOpen()) {
181+
if (this.closeHook != null) {
182+
try {
183+
this.closeHook.call();
184+
} catch (Exception e) {
185+
LOGGER.warn("Exception occured during close", e);
186+
}
187+
// log and swallow this exception closing
188+
}
182189
}
183-
// We also need to invalidate ourselves so we don't continue allowing new messages through when disconnecting.
184-
this.isOpen = false;
185190
}
186191

187192
@Override
@@ -220,9 +225,15 @@ public void didCloseUnexpectedly() {
220225
}
221226
}
222227

223-
private void checkEndpointOpen() {
228+
private synchronized void checkEndpointOpen() {
224229
if (!this.isOpen) {
225230
throw new IllegalStateException("Endpoint closed");
226231
}
227232
}
233+
234+
private synchronized boolean closeIfOpen() {
235+
boolean wasOpen = this.isOpen;
236+
this.isOpen = false;
237+
return wasOpen;
238+
}
228239
}

tc-server/src/main/java/com/tc/objectserver/entity/ManagedEntityImpl.java

Lines changed: 42 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -976,16 +976,27 @@ private void getEntity(ServerEntityRequest getEntityRequest, ResultCapture respo
976976
if (this.isDestroyed) {
977977
response.failure(ServerException.createNotFoundException(getID()));
978978
} else {
979-
if (canDelete) {
980-
clientReferenceCount += 1;
981-
Assert.assertTrue(clientReferenceCount > 0);
982-
}
983979
// The FETCH can only come directly from a client so we can down-cast.
984980
ClientID clientID = getEntityRequest.getNodeID();
985981
ClientDescriptorImpl descriptor = new ClientDescriptorImpl(clientID, getEntityRequest.getClientInstance());
986982
boolean added = clientEntityStateManager.addReference(descriptor, this.fetchID);
983+
984+
if (canDelete) {
985+
if (added) {
986+
clientReferenceCount += 1;
987+
Assert.assertTrue(clientReferenceCount > 0);
988+
} else {
989+
// this should never happen. Ther server is expecting sane things from the client.
990+
logger.warn("the client has attempted to fetch the same entity instance twice {}", descriptor);
991+
}
992+
}
993+
987994
if (this.isInActiveState) {
988-
Assert.assertTrue(added);
995+
if (!added) {
996+
// Exception the client. Don't crash the server
997+
response.failure(ServerException.createReferencedException(id));
998+
return;
999+
}
9891000
// Fire the event that the client fetched the entity.
9901001
this.eventCollector.clientDidFetchEntity(clientID, this.id, this.consumerID, getEntityRequest.getClientInstance());
9911002
// finally notify the entity that it was fetched
@@ -1006,10 +1017,18 @@ private void getEntity(ServerEntityRequest getEntityRequest, ResultCapture respo
10061017
handler.handleReconnect(descriptor, extendedData);
10071018
}
10081019
} catch (ReconnectRejectedException rejected) {
1020+
Assert.assertTrue(clientEntityStateManager.removeReference(descriptor));
1021+
if (canDelete) {
1022+
clientReferenceCount -= 1;
1023+
}
10091024
response.failure(ServerException.createReconnectRejected(getID(), rejected));
10101025
return;
10111026
} catch (Exception e) {
10121027
// something happened during reconnection, force a disconnection, see ProcessTransactionHandler.disconnectClientDueToFailure for handling
1028+
Assert.assertTrue(clientEntityStateManager.removeReference(descriptor));
1029+
if (canDelete) {
1030+
clientReferenceCount -= 1;
1031+
}
10131032
logger.warn("unexpected exception. rejecting reconnection of " + descriptor.getNodeID() + " to " + this.id, e);
10141033
response.failure(ServerException.createReconnectRejected(getID(), new ReconnectRejectedException(e.getMessage(), e)));
10151034
return;
@@ -1031,17 +1050,27 @@ private void releaseEntity(ServerEntityRequest request, ResultCapture response)
10311050
if (this.isDestroyed) {
10321051
response.failure(ServerException.createNotFoundException(this.getID()));
10331052
} else {
1034-
if (canDelete) {
1035-
clientReferenceCount -= 1;
1036-
Assert.assertTrue(clientReferenceCount >= 0);
1037-
}
1038-
10391053
ClientID clientID = request.getNodeID();
10401054
ClientDescriptorImpl clientInstance = new ClientDescriptorImpl(clientID, request.getClientInstance());
10411055
boolean removed = clientEntityStateManager.removeReference(clientInstance);
10421056

1057+
if (canDelete) {
1058+
if (removed) {
1059+
clientReferenceCount -= 1;
1060+
Assert.assertTrue(clientReferenceCount >= 0);
1061+
} else {
1062+
// this should never happen. the expectation is that the client will send sane things to the server.
1063+
logger.warn("client has tried to release an entity that is not fetched {}", clientInstance);
1064+
}
1065+
}
1066+
10431067
if (this.isInActiveState) {
1044-
Assert.assertTrue(removed);
1068+
if (!removed) {
1069+
// Exception the client. don't crash the server
1070+
response.failure(ServerException.createNotFoundException(id));
1071+
return;
1072+
}
1073+
10451074
this.activeServerEntity.disconnected(clientInstance);
10461075
// Fire the event that the client released the entity.
10471076
this.eventCollector.clientDidReleaseEntity(clientID, this.id, this.consumerID, request.getClientInstance());
@@ -1077,6 +1106,8 @@ public Runnable promoteEntity() throws ConfigurationException {
10771106
// up on passives
10781107
if (canDelete) {
10791108
this.clientReferenceCount = 0;
1109+
// the entity references should have been cleared
1110+
Assert.assertTrue(clientEntityStateManager.verifyNoEntityReferences(fetchID));
10801111
} else {
10811112
Assert.assertEquals(this.clientReferenceCount, ManagedEntityImpl.UNDELETABLE_ENTITY);
10821113
}

tc-server/src/test/java/com/tc/objectserver/entity/ManagedEntityImplTest.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ public void setUp() throws Exception {
173173
messageSelf = mock(Sink.class);
174174
when(clientEntityStateManager.addReference(any(ClientDescriptorImpl.class), any(FetchID.class))).thenReturn(Boolean.TRUE);
175175
when(clientEntityStateManager.removeReference(any(ClientDescriptorImpl.class))).thenReturn(Boolean.TRUE);
176+
when(clientEntityStateManager.verifyNoEntityReferences(any())).thenReturn(Boolean.TRUE);
176177
eventCollector = new ManagementTopologyEventCollector(mock(IMonitoringProducer.class));
177178
// We will start this in a passive state, as the general test case.
178179
boolean isInActiveState = false;

0 commit comments

Comments
 (0)