Skip to content

Commit 25b440e

Browse files
author
Myron Scott
committed
harden client references so the server is not crashed due to client bugs
1 parent 4415c58 commit 25b440e

File tree

5 files changed

+117
-41
lines changed

5 files changed

+117
-41
lines changed

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

Lines changed: 23 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

@@ -409,6 +408,23 @@ private <M extends EntityMessage, R extends EntityResponse> EntityClientEndpoint
409408
Assert.assertNotNull("Can't lookup null entity descriptor", instance);
410409
final EntityDescriptor fetchDescriptor = EntityDescriptor.createDescriptorForFetch(entity, version, instance);
411410
EntityClientEndpointImpl<M, R> resolvedEndpoint = null;
411+
// make sure this close hook is only ever called once
412+
CloseHookCallable closeCall = new CloseHookCallable() {
413+
boolean released = false;
414+
@Override
415+
public EntityException call() {
416+
if (!released) {
417+
released = true;
418+
try {
419+
internalRelease(entity, fetchDescriptor);
420+
} catch (EntityException e) {
421+
// We aren't expecting there to be any problems releasing an entity in the close hook but return the exception if there is an issue.
422+
return e;
423+
}
424+
}
425+
return null;
426+
}
427+
};
412428
try {
413429
byte[] raw = internalRetrieve(fetchDescriptor);
414430
ByteBuffer br = ByteBuffer.wrap(raw);
@@ -418,21 +434,8 @@ private <M extends EntityMessage, R extends EntityResponse> EntityClientEndpoint
418434
br.get(config);
419435
// We can only fail to get the config if we threw an exception.
420436
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);
437+
438+
resolvedEndpoint = new EntityClientEndpointImpl<>(entity, version, EntityDescriptor.createDescriptorForInvoke(fetch, instance), this, config, codec, closeCall, this.endpointCloser);
436439

437440
if (this.objectStoreMap.putIfAbsent(instance, resolvedEndpoint) != null) {
438441
throw Assert.failure("Attempt to add an object that already exists: Object of class " + resolvedEndpoint.getClass()
@@ -442,14 +445,14 @@ public void run() {
442445
throw notfound;
443446
} catch (EntityException e) {
444447
// 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.
448+
e.addSuppressed(closeCall.call());
449+
447450
throw e;
448451
} catch (Throwable t) {
449452
// This is the unexpected case so clean up and re-throw as a RuntimeException
450453
// 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.
454+
t.addSuppressed(closeCall.call());
455+
453456
throw Throwables.propagate(t);
454457
}
455458

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
*
3+
* The contents of this file are subject to the Terracotta Public License Version
4+
* 2.0 (the "License"); You may not use this file except in compliance with the
5+
* License. You may obtain a copy of the License at
6+
*
7+
* http://terracotta.org/legal/terracotta-public-license.
8+
*
9+
* Software distributed under the License is distributed on an "AS IS" basis,
10+
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
11+
* the specific language governing rights and limitations under the License.
12+
*
13+
* The Covered Software is Terracotta Core.
14+
*
15+
* The Initial Developer of the Covered Software is
16+
* Terracotta, Inc., a Software AG company
17+
*
18+
*/
19+
package com.tc.object;
20+
21+
import java.util.concurrent.Callable;
22+
import org.terracotta.exception.EntityException;
23+
24+
/**
25+
*
26+
*/
27+
public interface CloseHookCallable extends Callable<EntityException> {
28+
@Override
29+
EntityException call();
30+
}

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

Lines changed: 19 additions & 10 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 CloseHookCallable 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, CloseHookCallable closeHook, ExecutorService closer) {
7071
this.entityID = eid;
7172
this.version = version;
7273
this.invokeDescriptor = instance;
@@ -96,14 +97,14 @@ EntityDescriptor getEntityDescriptor() {
9697
@Override
9798
public byte[] getEntityConfiguration() {
9899
// This is harmless while closed but shouldn't be called so check open.
99-
checkEndpointOpen();
100+
checkEndpointOpen(false);
100101
return configuration;
101102
}
102103

103104
@Override
104105
public void setDelegate(EndpointDelegate<R> delegate) {
105106
// This is harmless while closed but shouldn't be called so check open.
106-
checkEndpointOpen();
107+
checkEndpointOpen(false);
107108
Assert.assertNull(this.delegate);
108109
this.delegate = delegate;
109110
}
@@ -124,7 +125,7 @@ public InFlightStats getStatistics() {
124125
@Override
125126
public Invocation<R> message(M message) {
126127
// We can't create new invocations when the endpoint is closed.
127-
checkEndpointOpen();
128+
checkEndpointOpen(false);
128129
return new InvocationImpl(message);
129130
}
130131

@@ -176,12 +177,17 @@ public byte[] getExtendedReconnectData() {
176177
@Override
177178
public void close() {
178179
// We can't close twice.
179-
checkEndpointOpen();
180+
try {
181+
checkEndpointOpen(true);
182+
} catch (IllegalStateException alreadyClosed) {
183+
LOGGER.info("Trying to close an endpoint which has already been closed", alreadyClosed);
184+
return;
185+
}
180186
if (this.closeHook != null) {
181-
this.closeHook.run();
187+
EntityException e = this.closeHook.call();
188+
LOGGER.info("Exception occured during close", e);
189+
// log and swallow this exception closing
182190
}
183-
// We also need to invalidate ourselves so we don't continue allowing new messages through when disconnecting.
184-
this.isOpen = false;
185191
}
186192

187193
@Override
@@ -220,9 +226,12 @@ public void didCloseUnexpectedly() {
220226
}
221227
}
222228

223-
private void checkEndpointOpen() {
229+
private synchronized void checkEndpointOpen(boolean close) {
224230
if (!this.isOpen) {
225231
throw new IllegalStateException("Endpoint closed");
226232
}
233+
if (close) {
234+
this.isOpen = false;
235+
}
227236
}
228237
}

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

Lines changed: 44 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -976,16 +976,28 @@ 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.info("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+
// this should never happen if the client is sane but if it does, exception the client
997+
// don't crash the server
998+
response.failure(ServerException.createReferencedException(id));
999+
return;
1000+
}
9891001
// Fire the event that the client fetched the entity.
9901002
this.eventCollector.clientDidFetchEntity(clientID, this.id, this.consumerID, getEntityRequest.getClientInstance());
9911003
// finally notify the entity that it was fetched
@@ -1006,10 +1018,18 @@ private void getEntity(ServerEntityRequest getEntityRequest, ResultCapture respo
10061018
handler.handleReconnect(descriptor, extendedData);
10071019
}
10081020
} catch (ReconnectRejectedException rejected) {
1021+
Assert.assertTrue(clientEntityStateManager.removeReference(descriptor));
1022+
if (canDelete) {
1023+
clientReferenceCount -= 1;
1024+
}
10091025
response.failure(ServerException.createReconnectRejected(getID(), rejected));
10101026
return;
10111027
} catch (Exception e) {
10121028
// something happened during reconnection, force a disconnection, see ProcessTransactionHandler.disconnectClientDueToFailure for handling
1029+
Assert.assertTrue(clientEntityStateManager.removeReference(descriptor));
1030+
if (canDelete) {
1031+
clientReferenceCount -= 1;
1032+
}
10131033
logger.warn("unexpected exception. rejecting reconnection of " + descriptor.getNodeID() + " to " + this.id, e);
10141034
response.failure(ServerException.createReconnectRejected(getID(), new ReconnectRejectedException(e.getMessage(), e)));
10151035
return;
@@ -1031,17 +1051,28 @@ private void releaseEntity(ServerEntityRequest request, ResultCapture response)
10311051
if (this.isDestroyed) {
10321052
response.failure(ServerException.createNotFoundException(this.getID()));
10331053
} else {
1034-
if (canDelete) {
1035-
clientReferenceCount -= 1;
1036-
Assert.assertTrue(clientReferenceCount >= 0);
1037-
}
1038-
10391054
ClientID clientID = request.getNodeID();
10401055
ClientDescriptorImpl clientInstance = new ClientDescriptorImpl(clientID, request.getClientInstance());
10411056
boolean removed = clientEntityStateManager.removeReference(clientInstance);
10421057

1058+
if (canDelete) {
1059+
if (removed) {
1060+
clientReferenceCount -= 1;
1061+
Assert.assertTrue(clientReferenceCount >= 0);
1062+
} else {
1063+
// this should never happen. the expectation is that the client will send sane things to the server. Will assert if active
1064+
logger.info("client has tried to release and entity that is not fetched {}", clientInstance);
1065+
}
1066+
}
1067+
10431068
if (this.isInActiveState) {
1044-
Assert.assertTrue(removed);
1069+
if (!removed) {
1070+
// this should never happen if the client is sane but if it does, exception the client
1071+
// don't crash the server
1072+
response.failure(ServerException.createNotFoundException(id));
1073+
return;
1074+
}
1075+
10451076
this.activeServerEntity.disconnected(clientInstance);
10461077
// Fire the event that the client released the entity.
10471078
this.eventCollector.clientDidReleaseEntity(clientID, this.id, this.consumerID, request.getClientInstance());
@@ -1077,6 +1108,8 @@ public Runnable promoteEntity() throws ConfigurationException {
10771108
// up on passives
10781109
if (canDelete) {
10791110
this.clientReferenceCount = 0;
1111+
// the entity references should have been cleared
1112+
Assert.assertTrue(clientEntityStateManager.verifyNoEntityReferences(fetchID));
10801113
} else {
10811114
Assert.assertEquals(this.clientReferenceCount, ManagedEntityImpl.UNDELETABLE_ENTITY);
10821115
}

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)