Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix an intermittent deadlock when DocService is loaded for a Thrift service #4688

Merged
merged 20 commits into from
Mar 27, 2023
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,6 @@ static ExceptionInfo newExceptionInfo(Class<? extends TException> exceptionClass
return new ExceptionInfo(name, fields);
}

@VisibleForTesting
static FieldInfo newFieldInfo(Class<?> parentType, FieldMetaData fieldMetaData) {
requireNonNull(fieldMetaData, "fieldMetaData");
final FieldValueMetaData fieldValueMetaData = fieldMetaData.valueMetaData;
Expand Down Expand Up @@ -318,8 +317,7 @@ private static boolean hasTypeDef(FieldValueMetaData valueMetadata) {
static <T extends TBase<T, F>, F extends TFieldIdEnum> StructInfo newStructInfo(Class<?> structClass) {
final String name = structClass.getName();

//noinspection unchecked
final Map<?, FieldMetaData> metaDataMap = FieldMetaData.getStructMetaDataMap((Class<T>) structClass);
final Map<?, FieldMetaData> metaDataMap = ThriftMetadataAccess.getStructMetaDataMap(structClass);
final List<FieldInfo> fields =
metaDataMap.values().stream()
.map(fieldMetaData -> newFieldInfo(structClass, fieldMetaData))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -239,18 +239,16 @@ private static <T extends TBase<T, F>, F extends TFieldIdEnum> MethodInfo newMet
requireNonNull(exceptionClasses, "exceptionClasses");
requireNonNull(endpoints, "endpoints");

//noinspection unchecked,RedundantCast
final List<FieldInfo> parameters =
FieldMetaData.getStructMetaDataMap((Class<T>) argsClass).values().stream()
.map(fieldMetaData -> newFieldInfo(argsClass, fieldMetaData))
.collect(toImmutableList());
ThriftMetadataAccess.getStructMetaDataMap(argsClass).values().stream()
.map(fieldMetaData -> newFieldInfo(argsClass, fieldMetaData))
.collect(toImmutableList());

// Find the 'success' field.
FieldInfo fieldInfo = null;
if (resultClass != null) { // Function isn't "oneway" function
//noinspection unchecked,RedundantCast
final Map<? extends TFieldIdEnum, FieldMetaData> resultMetaData =
FieldMetaData.getStructMetaDataMap((Class<T>) resultClass);
final Map<?, FieldMetaData> resultMetaData =
ThriftMetadataAccess.getStructMetaDataMap(resultClass);

for (FieldMetaData fieldMetaData : resultMetaData.values()) {
if ("success".equals(fieldMetaData.fieldName)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/*
* Copyright 2023 LINE Corporation
*
* LINE Corporation 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:
*
* https://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 com.linecorp.armeria.internal.server.thrift;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.URL;
import java.util.Enumeration;
import java.util.Map;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.thrift.TBase;
import org.apache.thrift.TFieldIdEnum;
import org.apache.thrift.meta_data.FieldMetaData;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.annotations.VisibleForTesting;

final class ThriftMetadataAccess {

private static final Logger logger = LoggerFactory.getLogger(ThriftMetadataAccess.class);

private static boolean preInitializeThriftClass;
private static final Pattern preInitializeTargetPattern =
Pattern.compile("^armeria-thrift0\\.(\\d+)\\..*$");

static {
try {
final Enumeration<URL> versionPropertiesUrls =
ThriftMetadataAccess.class.getClassLoader().getResources(
"META-INF/com.linecorp.armeria.versions.properties");
Copy link
Member

@trustin trustin Mar 7, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Can't we just do Version.getAll(...).keySet() to get the list of the artifact IDs?
  • Can't we just always pre-initialize the classes?
  • What do you think about adding an empty resource file like src/main/resources/com/linecorp/armeria/internal/common/thrift/requires_struct_preinit to all thrift modules older than 0.15? We could enable the workaround only if that resource file exists.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given the cost of failure to look for a resource file might be a little bit higher, we could always add a properties file:

If pre-init is required:

structPreinitRequired=true

If pre-init is not required:

<empty file>

Copy link
Contributor Author

@jrhee17 jrhee17 Mar 8, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't we just do Version.getAll(...).keySet() to get the list of the artifact IDs?

Didn't know this class existed 😅 It's probably better to use this.

Can't we just always pre-initialize the classes?

I think I prefer making an explicit branch statement for these type of changes:

  1. If we decide to deprecate thrift <= 0.14 modules, we can remove the related logic safely
  2. The reason for pre-initialization is embedded in the code

I do agree that the logic doesn't really affect performance and am open to just preinitializing always if others feel this is simpler.

What do you think about adding an empty resource file
we could always add a properties file

I don't even think this has to be a resource, but it can also be a class file that has a different static value for each thrift module. <- On second thought, this doesn't work if multiple armeria-thrift artifacts are added.
I didn't want to further complicate the thrift file copying logic, but I think this is also an option.

I think I prefer just using Versions.all() like you mentioned, but let me know if you feel differently.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this doesn't work if multiple armeria-thrift artifacts are added.

Doesn't a user have a bigger problem if their classpath has multiple armeria-thrift artifacts? We could leave a warning and fall back to safe mode (always preinit)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't even think this has to be a resource, but it can also be a class file that has a different static value for each thrift module. <- On second thought, this doesn't work if multiple armeria-thrift artifacts are added.

We could leave a warning and fall back to safe mode (always preinit)?

I understood what you meant to be leave a warning if multiple classes exist, am I understanding correctly?
I didn't think it was possible to detect if there are multiple classes with the same package/name/module.

This won't be a problem if we define a separate resource instead of class, but I don't see much benefit in using a dedicated resource over just Versions.all().

(always preinit)?

or did you mean to just always preinit? 😅

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't think it was possible to detect if there are multiple classes with the same package/name/module.

You can actually:

Enumeration<URL> urls = getClass().getClassLoader().getResources("com/foo/bar.txt"); // or .class
while (urls.hasMoreElements()) {
    URL url = urls.nextElement();
    InputStream inputStream = url.openStream();
    // do something with the input stream
}

Otherwise, how could Versions.all() fetch the com.linecorp.armeria.versions.properties files from all artifacts?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By the way, I meant putting some .properties file rather than looking for .class files. I think having a separate resource is still better than extracting version number from artifact ID because it doesn't require any string matching, which could be broken at some point.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think having a separate resource is still better than extracting version number from artifact ID because it doesn't require any string matching, which could be broken at some point.

I see 😄 Understood.

if (!versionPropertiesUrls.hasMoreElements()) {
// versions.properties was not found
logger.trace("Unable to determine the 'armeria-thrift' version. Please consider " +
"adding 'META-INF/com.linecorp.armeria.versions.properties' to the " +
"classpath to avoid unexpected issues.");
}
boolean preInitializeThriftClass = false;
while (versionPropertiesUrls.hasMoreElements()) {
final URL url = versionPropertiesUrls.nextElement();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(url.openStream()))) {
final Properties props = new Properties();
props.load(reader);
preInitializeThriftClass = needsPreInitialization(props);
if (preInitializeThriftClass) {
break;
}
}
}
ThriftMetadataAccess.preInitializeThriftClass = preInitializeThriftClass;
} catch (Exception e) {
logger.debug("Unexpected exception while determining the 'armeria-thrift' version: ", e);
}
}

@VisibleForTesting
static boolean needsPreInitialization(Properties props) {
for (String key : props.stringPropertyNames()) {
final Matcher matcher = preInitializeTargetPattern.matcher(key);
if (!matcher.matches()) {
continue;
}
final int version = Integer.parseInt(matcher.group(1));
if (version <= 14) {
logger.trace("Pre-initializing thrift metadata due to 'armeria-thrift0.{}'", version);
return true;
}
}
return false;
}

@SuppressWarnings("unchecked")
public static <T extends TBase<T, F>, F extends TFieldIdEnum>
Map<?, FieldMetaData> getStructMetaDataMap(Class<?> clazz) {
// Pre-initialize classes if there is a jar in the classpath with armeria-thrift <= 0.14
// See the following issue for the motivation of pre-initializing classes
// https://issues.apache.org/jira/browse/THRIFT-5430
if (preInitializeThriftClass) {
try {
Class.forName(clazz.getName(), true, clazz.getClassLoader());
} catch (ClassNotFoundException e) {
// Another exception will probably be raised in the actual getStructMetaDataMap so just
// logging for at this point.
logger.trace("Unexpected exception while initializing class {}: ", clazz, e);
}
}
return FieldMetaData.getStructMetaDataMap((Class<T>) clazz);
}

private ThriftMetadataAccess() {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*
* Copyright 2023 LINE Corporation
*
* LINE Corporation 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:
*
* https://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 com.linecorp.armeria.internal.server.thrift;

import static org.assertj.core.api.Assertions.assertThat;

import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

import org.junit.jupiter.api.Test;

import net.bytebuddy.dynamic.ClassFileLocator.ForClassLoader;

import com.linecorp.armeria.service.test.thrift.main.FooStruct;

class ThriftClassLoadingTest {

// see https://issues.apache.org/jira/browse/THRIFT-5430
@Test
void testDeadlock() throws Exception {
for (int i = 0; i < 20; i++) {
final ExecutorService e1 = Executors.newSingleThreadExecutor();
final ExecutorService e2 = Executors.newSingleThreadExecutor();
final ClassLoader classLoader = new SimpleClassLoader(FooStruct.class);
@SuppressWarnings("unchecked")
final Class<FooStruct> aClass =
(Class<FooStruct>) Class.forName(FooStruct.class.getName(), false, classLoader);
e1.submit(() -> ThriftDescriptiveTypeInfoProvider.newStructInfo(aClass));
e2.submit(() -> Class.forName(FooStruct.class.getName(), true, classLoader));
e1.shutdown();
e2.shutdown();
// unfortunately if a deadlock did occur the threads are in an uninterruptible state
// which means the threads cannot be cleaned up
assertThat(e1.awaitTermination(10, TimeUnit.SECONDS)).isTrue();
assertThat(e2.awaitTermination(10, TimeUnit.SECONDS)).isTrue();
}
}

/**
* A simple class loader used to re-initialize a class.
*/
private static class SimpleClassLoader extends ClassLoader {

private final Class<?> targetClass;
private final Map<String, Class<?>> memo = new HashMap<>();

SimpleClassLoader(Class<?> targetClass) {
this.targetClass = targetClass;
}

@Override
public synchronized Class<?> loadClass(String name) throws ClassNotFoundException {
if (!name.startsWith(targetClass.getName())) {
return super.loadClass(name);
}
if (memo.containsKey(name)) {
return memo.get(name);
}
final byte[] bytes = ForClassLoader.read(Class.forName(name));
final Class<?> clazz = defineClass(name, bytes, 0, bytes.length, null);
Arrays.stream(clazz.getConstructors()).forEach(c -> c.setAccessible(true));
memo.put(name, clazz);
return clazz;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright 2023 LINE Corporation
*
* LINE Corporation 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:
*
* https://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 com.linecorp.armeria.internal.server.thrift;

import static org.assertj.core.api.Assertions.assertThat;

import java.util.Properties;

import org.junit.jupiter.api.Test;

class ThriftMetadataAccessTest {

@Test
void testPreInitializeTarget() {
final Properties properties = new Properties();
properties.put("armeria-thrift0.13.shortCommitHash", "c533c7fd3");
assertThat(ThriftMetadataAccess.needsPreInitialization(properties)).isTrue();

properties.clear();
properties.put("armeria-thrift0.15.shortCommitHash", "c533c7fd3");
assertThat(ThriftMetadataAccess.needsPreInitialization(properties)).isFalse();
}
}