-
Notifications
You must be signed in to change notification settings - Fork 913
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
Changes from 9 commits
Commits
Show all changes
20 commits
Select commit
Hold shift + click to select a range
64b0575
add a failing test
jrhee17 11ea0a4
fix lint failures
jrhee17 06bff92
clean up deadlocked tasks on test failure
jrhee17 f95e1e8
add a comment on cleanup
jrhee17 44da42c
pre-initialize classes for thrift modules under 0.14
jrhee17 a809fcb
minor cleanups
jrhee17 3139112
fix a compilation error
jrhee17 c29e06d
fix lint
jrhee17 7865893
address comments by @ikhoon
jrhee17 42bb808
use the Versions api
jrhee17 aef84ac
increase number of iterations so a deadlock occurs more frequently
jrhee17 916385c
handle lint errors
jrhee17 8dcd48f
lint
jrhee17 3d6f6d2
address comments by @trustin
jrhee17 2ec578d
add missing resource copy
jrhee17 fe618d0
lint
jrhee17 fcb89c2
Merge branch 'main' of github.com:line/armeria into bugfix/thrift-doc…
jrhee17 180517d
address comments by @trustin
jrhee17 901f9ef
address comments by @ikhoon
jrhee17 e9e82a7
test failure
jrhee17 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
108 changes: 108 additions & 0 deletions
108
...t0.13/src/main/java/com/linecorp/armeria/internal/server/thrift/ThriftMetadataAccess.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"); | ||
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() {} | ||
} |
84 changes: 84 additions & 0 deletions
84
....13/src/test/java/com/linecorp/armeria/internal/server/thrift/ThriftClassLoadingTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} | ||
} |
37 changes: 37 additions & 0 deletions
37
...3/src/test/java/com/linecorp/armeria/internal/server/thrift/ThriftMetadataAccessTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
} |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Version.getAll(...).keySet()
to get the list of the artifact IDs?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.There was a problem hiding this comment.
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:
If pre-init is not required:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Didn't know this class existed 😅 It's probably better to use this.
I think I prefer making an explicit branch statement for these type of changes:
I do agree that the logic doesn't really affect performance and am open to just preinitializing always if others feel this is simpler.
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.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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)?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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()
.or did you mean to just always preinit? 😅
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can actually:
Otherwise, how could
Versions.all()
fetch thecom.linecorp.armeria.versions.properties
files from all artifacts?There was a problem hiding this comment.
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.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see 😄 Understood.