Skip to content

Commit 0d0286a

Browse files
IGNITE-15256 Java Thin: Fix ClassNotFoundException on service call after failover (apache#9307)
Clear thin client caches when cluster connection is lost. The root cause of the issue is that client believes that server nodes preserve registered user types after reconnect, however, it could be the case that server nodes lose this information (e.g. full cluster restart with clean up of working dirs). This leads to ClassNotFoundException.
1 parent a741705 commit 0d0286a

File tree

5 files changed

+194
-3
lines changed

5 files changed

+194
-3
lines changed

modules/core/src/main/java/org/apache/ignite/internal/binary/BinaryContext.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1409,6 +1409,9 @@ public void unregisterUserTypeDescriptors() {
14091409
if (e.getValue().userType())
14101410
it.remove();
14111411
}
1412+
1413+
// Unregister Serializable and Externalizable type descriptors.
1414+
optmMarsh.clearClassDescriptorsCache();
14121415
}
14131416

14141417
/**
@@ -1438,6 +1441,8 @@ public void onUndeploy(ClassLoader ldr) {
14381441
}
14391442
}
14401443

1444+
optmMarsh.onUndeploy(ldr);
1445+
14411446
U.clearClassCache(ldr);
14421447
}
14431448

modules/core/src/main/java/org/apache/ignite/internal/client/thin/TcpIgniteClient.java

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,8 @@ private TcpIgniteClient(ClientConfiguration cfg) throws ClientException {
111111
) throws ClientException {
112112
final ClientBinaryMetadataHandler metadataHnd = new ClientBinaryMetadataHandler();
113113

114-
marsh = new ClientBinaryMarshaller(metadataHnd, new ClientMarshallerContext());
114+
ClientMarshallerContext marshCtx = new ClientMarshallerContext();
115+
marsh = new ClientBinaryMarshaller(metadataHnd, marshCtx);
115116

116117
marsh.setBinaryConfiguration(cfg.getBinaryConfiguration());
117118

@@ -124,7 +125,14 @@ private TcpIgniteClient(ClientConfiguration cfg) throws ClientException {
124125
try {
125126
ch.channelsInit();
126127

127-
ch.addChannelFailListener(metadataHnd::onReconnect);
128+
// Metadata, binary descriptors and user types caches must be cleared so that the
129+
// client will register all the user types within the cluster once again in case this information
130+
// was lost during the cluster failover.
131+
ch.addChannelFailListener(() -> {
132+
metadataHnd.onReconnect();
133+
marshCtx.clearUserTypesCache();
134+
marsh.context().unregisterUserTypeDescriptors();
135+
});
128136

129137
// Send postponed metadata after channel init.
130138
metadataHnd.sendAllMeta();
@@ -521,7 +529,7 @@ void onReconnect() {
521529
*/
522530
private class ClientMarshallerContext implements MarshallerContext {
523531
/** Type ID -> class name map. */
524-
private Map<Integer, String> cache = new ConcurrentHashMap<>();
532+
private final Map<Integer, String> cache = new ConcurrentHashMap<>();
525533

526534
/** System types. */
527535
private final Collection<String> sysTypes = new HashSet<>();
@@ -646,5 +654,14 @@ public ClientMarshallerContext() {
646654
@Override public JdkMarshaller jdkMarshaller() {
647655
return new JdkMarshaller();
648656
}
657+
658+
/**
659+
* Clear the user types cache on reconnect so that the client will register all
660+
* the types once again after reconnect.
661+
* See the comment in constructor of the TcpIgniteClient.
662+
*/
663+
void clearUserTypesCache() {
664+
cache.clear();
665+
}
649666
}
650667
}

modules/core/src/main/java/org/apache/ignite/internal/marshaller/optimized/OptimizedMarshaller.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,15 @@ public static boolean available() {
340340
U.clearClassCache(ldr);
341341
}
342342

343+
/**
344+
* Clears the optimized class descriptors cache. This is essential for the clients
345+
* on disconnect in order to make them register their user types again (server nodes may
346+
* lose previously registered types).
347+
*/
348+
public void clearClassDescriptorsCache() {
349+
clsMap.clear();
350+
}
351+
343352
/** {@inheritDoc} */
344353
@Override public String toString() {
345354
return S.toString(OptimizedMarshaller.class, this);
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package org.apache.ignite.client;
19+
20+
import java.io.Externalizable;
21+
import java.io.IOException;
22+
import java.io.ObjectInput;
23+
import java.io.ObjectOutput;
24+
import org.apache.ignite.internal.util.typedef.internal.S;
25+
26+
/**
27+
* An externalizable person entity for the tests.
28+
*/
29+
public class PersonExternalizable implements Externalizable {
30+
/** */
31+
private String name;
32+
33+
/**
34+
* Externalizable
35+
*/
36+
public PersonExternalizable() {
37+
}
38+
39+
/** */
40+
public PersonExternalizable(String name) {
41+
this.name = name;
42+
}
43+
44+
/** {@inheritDoc} */
45+
@Override public String toString() {
46+
return S.toString(PersonExternalizable.class, this);
47+
}
48+
49+
/** {@inheritDoc} */
50+
@Override public void writeExternal(ObjectOutput out) throws IOException {
51+
out.writeUTF(name);
52+
}
53+
54+
/** {@inheritDoc} */
55+
@Override public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
56+
name = in.readUTF();
57+
}
58+
}

modules/core/src/test/java/org/apache/ignite/client/ReliabilityTest.java

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,14 @@
3737
import org.apache.ignite.cache.query.Query;
3838
import org.apache.ignite.cache.query.QueryCursor;
3939
import org.apache.ignite.cache.query.ScanQuery;
40+
import org.apache.ignite.configuration.DataStorageConfiguration;
4041
import org.apache.ignite.failure.FailureHandler;
4142
import org.apache.ignite.internal.client.thin.AbstractThinClientTest;
4243
import org.apache.ignite.internal.client.thin.ClientServerError;
44+
import org.apache.ignite.internal.util.typedef.internal.U;
45+
import org.apache.ignite.services.Service;
46+
import org.apache.ignite.services.ServiceConfiguration;
47+
import org.apache.ignite.services.ServiceContext;
4348
import org.apache.ignite.testframework.GridTestUtils;
4449
import org.junit.Test;
4550

@@ -51,6 +56,9 @@
5156
* High Availability tests.
5257
*/
5358
public class ReliabilityTest extends AbstractThinClientTest {
59+
/** Service name. */
60+
private static final String SERVICE_NAME = "svc";
61+
5462
/**
5563
* Thin clint failover.
5664
*/
@@ -370,6 +378,69 @@ public void testServerCriticalError() throws Exception {
370378
}
371379
}
372380

381+
/**
382+
* Test that client can invoke service method with externalizable parameter after
383+
* cluster failover.
384+
*/
385+
@Test
386+
public void testServiceMethodInvocationAfterFailover() throws Exception {
387+
PersonExternalizable person = new PersonExternalizable("Person 1");
388+
389+
ServiceConfiguration testServiceConfig = new ServiceConfiguration();
390+
testServiceConfig.setName(SERVICE_NAME);
391+
testServiceConfig.setService(new TestService());
392+
testServiceConfig.setTotalCount(1);
393+
394+
Ignite ignite = null;
395+
IgniteClient client = null;
396+
try {
397+
// Initialize cluster and client
398+
ignite = startGrid(getConfiguration().setServiceConfiguration(testServiceConfig));
399+
client = startClient(ignite);
400+
TestServiceInterface svc = client.services().serviceProxy(SERVICE_NAME, TestServiceInterface.class);
401+
402+
// Invoke the service method with Externalizable parameter for the first time.
403+
// This triggers registration of the PersonExternalizable type in the cluter.
404+
String result = svc.testMethod(person);
405+
assertEquals("testMethod(PersonExternalizable person): " + person, result);
406+
407+
// Kill the cluster node, clean up the working directory (with cached types)
408+
// and drop the client connection.
409+
ignite.close();
410+
U.delete(U.resolveWorkDirectory(
411+
U.defaultWorkDirectory(),
412+
DataStorageConfiguration.DFLT_MARSHALLER_PATH,
413+
false));
414+
dropAllThinClientConnections();
415+
416+
// Invoke the service.
417+
GridTestUtils.assertThrowsWithCause(() -> svc.testMethod(person), ClientConnectionException.class);
418+
419+
// Restore the cluster and redeploy the service.
420+
ignite = startGrid(getConfiguration().setServiceConfiguration(testServiceConfig));
421+
422+
// Invoke the service method with Externalizable parameter once again.
423+
// This should restore the client connection and trigger registration of the
424+
// PersonExternalizable once again.
425+
result = svc.testMethod(person);
426+
assertEquals("testMethod(PersonExternalizable person): " + person, result);
427+
} finally {
428+
if (ignite != null) {
429+
try {
430+
ignite.close();
431+
} catch (Throwable ignore) {
432+
}
433+
}
434+
435+
if (client != null) {
436+
try {
437+
client.close();
438+
} catch (Throwable ignore) {
439+
}
440+
}
441+
}
442+
}
443+
373444
/**
374445
* Performs cache put.
375446
*
@@ -427,4 +498,35 @@ private void assertOnUnstableCluster(LocalIgniteCluster cluster, Runnable clo) t
427498
protected boolean isPartitionAware() {
428499
return false;
429500
}
501+
502+
/** */
503+
public static interface TestServiceInterface {
504+
/** */
505+
public String testMethod(PersonExternalizable person);
506+
}
507+
508+
/**
509+
* Implementation of TestServiceInterface.
510+
*/
511+
public static class TestService implements Service, TestServiceInterface {
512+
/** {@inheritDoc} */
513+
@Override public void cancel(ServiceContext ctx) {
514+
// No-op.
515+
}
516+
517+
/** {@inheritDoc} */
518+
@Override public void init(ServiceContext ctx) throws Exception {
519+
// No-op.
520+
}
521+
522+
/** {@inheritDoc} */
523+
@Override public void execute(ServiceContext ctx) throws Exception {
524+
// No-op.
525+
}
526+
527+
/** {@inheritDoc} */
528+
@Override public String testMethod(PersonExternalizable person) {
529+
return "testMethod(PersonExternalizable person): " + person;
530+
}
531+
}
430532
}

0 commit comments

Comments
 (0)