-
Notifications
You must be signed in to change notification settings - Fork 2
VSB-TUO/ORCID Authority fix - do NOT call context.abort() on a locally created Context #1261
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
Changes from all commits
aa27ccc
da0ee37
3df3d45
ebb57f6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -152,11 +152,10 @@ public String getLabel(String key, String locale) { | |||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| private String resolveLocalLabel(String key, String locale) { | ||||||||||||||||||||||||||||||||||||||||||
| Context requestContext = ContextUtil.obtainCurrentRequestContext(); | ||||||||||||||||||||||||||||||||||||||||||
| boolean createdContext = (requestContext == null); | ||||||||||||||||||||||||||||||||||||||||||
| Context context = requestContext; | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||
| if (createdContext) { | ||||||||||||||||||||||||||||||||||||||||||
| if (context == null) { | ||||||||||||||||||||||||||||||||||||||||||
| context = createReadOnlyContext(); | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| if (context == null) { | ||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -166,11 +165,16 @@ private String resolveLocalLabel(String key, String locale) { | |||||||||||||||||||||||||||||||||||||||||
| } catch (Exception e) { | ||||||||||||||||||||||||||||||||||||||||||
| log.error("Error resolving local label for authority key '{}'", key, e); | ||||||||||||||||||||||||||||||||||||||||||
| return key; | ||||||||||||||||||||||||||||||||||||||||||
| } finally { | ||||||||||||||||||||||||||||||||||||||||||
| if (createdContext && context != null) { | ||||||||||||||||||||||||||||||||||||||||||
| context.abort(); | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| // We intentionally do NOT call context.abort() on a locally created Context. | ||||||||||||||||||||||||||||||||||||||||||
| // During CLI operations (e.g. reindexing), the new Context shares the Hibernate | ||||||||||||||||||||||||||||||||||||||||||
| // session (thread-local) with the caller's Context. Calling abort() triggers | ||||||||||||||||||||||||||||||||||||||||||
| // closeDBConnection() → rollback(), which kills the shared transaction and causes | ||||||||||||||||||||||||||||||||||||||||||
| // Hibernate to clear the persistence context — detaching ALL managed entities. | ||||||||||||||||||||||||||||||||||||||||||
| // This leads to LazyInitializationException when the caller later accesses | ||||||||||||||||||||||||||||||||||||||||||
| // lazy-loaded properties (e.g. DSpaceObject.handles). | ||||||||||||||||||||||||||||||||||||||||||
| // Since we only performed read operations, no cleanup is needed. | ||||||||||||||||||||||||||||||||||||||||||
| // The session/transaction lifecycle is managed by the caller's Context. | ||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+169
to
+177
|
||||||||||||||||||||||||||||||||||||||||||
| // We intentionally do NOT call context.abort() on a locally created Context. | |
| // During CLI operations (e.g. reindexing), the new Context shares the Hibernate | |
| // session (thread-local) with the caller's Context. Calling abort() triggers | |
| // closeDBConnection() → rollback(), which kills the shared transaction and causes | |
| // Hibernate to clear the persistence context — detaching ALL managed entities. | |
| // This leads to LazyInitializationException when the caller later accesses | |
| // lazy-loaded properties (e.g. DSpaceObject.handles). | |
| // Since we only performed read operations, no cleanup is needed. | |
| // The session/transaction lifecycle is managed by the caller's Context. | |
| // We intentionally do NOT call context.abort() here, even when we created a | |
| // local READ_ONLY Context. In CLI-style flows (e.g. reindexing) a newly created | |
| // Context may share the underlying, thread-local Hibernate Session/connection | |
| // with an outer caller Context. For READ_ONLY modes, Context.abort() skips any | |
| // rollback but still closes the DB connection backing that shared Session. | |
| // Closing that connection while the outer Context is still in use can break the | |
| // caller's transaction and leave managed entities in an inconsistent state, | |
| // leading to LazyInitializationException when lazy properties are accessed later. | |
| // We only perform read operations here; the owning infrastructure (web request / | |
| // CLI command) is responsible for managing the Session/transaction lifecycle and | |
| // ultimately closing the associated Context(s). |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -358,7 +358,13 @@ public void updateIndex(Context context, boolean force, String type) { | |
| final Iterator<IndexableObject> indexableObjects = indexableObjectService.findAll(context); | ||
| while (indexableObjects.hasNext()) { | ||
| final IndexableObject indexableObject = indexableObjects.next(); | ||
| indexContent(context, indexableObject, force); | ||
| try { | ||
| indexContent(context, indexableObject, force); | ||
| } catch (RuntimeException e) { | ||
| log.error("Failed to index object {} (type={}): {}", | ||
| indexableObject.getUniqueIndexID(), indexableObject.getType(), | ||
| e.getMessage(), e); | ||
| } | ||
|
Comment on lines
+361
to
+367
|
||
| context.uncacheEntity(indexableObject.getIndexedObject()); | ||
| indexObject++; | ||
| if ((indexObject % 100) == 0 && indexableObjectService instanceof ItemIndexFactory) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -22,13 +22,15 @@ | |
| import org.dspace.content.Community; | ||
| import org.dspace.content.MetadataValue; | ||
| import org.dspace.content.service.ItemService; | ||
| import org.dspace.core.LegacyPluginServiceImpl; | ||
| import org.dspace.ctask.testing.MarkerTask; | ||
| import org.dspace.eperson.EPerson; | ||
| import org.dspace.util.DSpaceConfigurationInitializer; | ||
| import org.dspace.util.DSpaceKernelInitializer; | ||
| import org.dspace.xmlworkflow.storedcomponents.XmlWorkflowItem; | ||
| import org.junit.Test; | ||
| import org.junit.runner.RunWith; | ||
| import org.springframework.beans.factory.annotation.Autowired; | ||
| import org.springframework.test.context.ContextConfiguration; | ||
| import org.springframework.test.context.junit4.SpringRunner; | ||
|
|
||
|
|
@@ -46,6 +48,8 @@ public class WorkflowCurationIT | |
| extends AbstractIntegrationTestWithDatabase { | ||
| @Inject | ||
| private ItemService itemService; | ||
| @Autowired | ||
| private LegacyPluginServiceImpl legacyPluginService; | ||
|
|
||
| /** | ||
| * Basic smoke test of a curation task attached to a workflow step. | ||
|
|
@@ -57,6 +61,11 @@ public void curationTest() | |
| throws Exception { | ||
| context.turnOffAuthorisationSystem(); | ||
|
|
||
| // Reset the named plugin cache to avoid pollution from other tests | ||
| // (e.g. CreateMissingIdentifiersIT) that may have run before this one. | ||
| // See https://github.com/DSpace/DSpace/issues/8533 | ||
| legacyPluginService.clearNamedPluginClasses(); | ||
|
Comment on lines
25
to
+67
|
||
|
|
||
| //** GIVEN ** | ||
|
|
||
| // A submitter; | ||
|
|
||
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.
resolveLocalLabel() now creates a new read-only Context when there is no current request Context, but never closes it. Because Context/DBConnection are backed by a thread-local Hibernate Session, leaving this Context unclosed can leak an open Session/transaction when no caller Context exists, and it can also trigger Context.finalize() later (which may call abort() and close the shared Session while the caller is still using it). Please reintroduce deterministic cleanup for locally-created Contexts (e.g., only create/close a Context when you can guarantee it owns its Session, or use an isolated Session/DataSource-based lookup for the fallback query so it can be safely closed).