diff --git a/dspace-api/src/main/java/org/dspace/content/authority/SimpleORCIDAuthority.java b/dspace-api/src/main/java/org/dspace/content/authority/SimpleORCIDAuthority.java index 7d54f6d879b3..9a647c1792e0 100644 --- a/dspace-api/src/main/java/org/dspace/content/authority/SimpleORCIDAuthority.java +++ b/dspace-api/src/main/java/org/dspace/content/authority/SimpleORCIDAuthority.java @@ -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. } Context createReadOnlyContext() { diff --git a/dspace-api/src/main/java/org/dspace/core/HibernateDBConnection.java b/dspace-api/src/main/java/org/dspace/core/HibernateDBConnection.java index 7dae4a9d9812..f57ceb474e39 100644 --- a/dspace-api/src/main/java/org/dspace/core/HibernateDBConnection.java +++ b/dspace-api/src/main/java/org/dspace/core/HibernateDBConnection.java @@ -301,7 +301,13 @@ public void uncacheEntity(E entity) throws SQLExcep } else if (entity instanceof Bundle) { Bundle bundle = (Bundle) entity; - if (Hibernate.isInitialized(bundle.getBitstreams())) { + // Bundle.getBitstreams() creates a defensive copy via new ArrayList<>(bitstreams) + // which iterates the Hibernate proxy, triggering lazy loading unconditionally. + // Unlike Item.getBundles() which returns the raw proxy, we cannot safely call + // getBitstreams() when the bundle is detached from the session. + // Guard with session.contains(): if the bundle is still managed, + // lazy loading will work; if detached (e.g. after session.clear()), we skip. + if (getSession().contains(bundle)) { for (Bitstream bitstream : Utils.emptyIfNull(bundle.getBitstreams())) { uncacheEntity(bitstream); } diff --git a/dspace-api/src/main/java/org/dspace/discovery/SolrServiceImpl.java b/dspace-api/src/main/java/org/dspace/discovery/SolrServiceImpl.java index b94aa61d3e4f..e1b90a8103f5 100644 --- a/dspace-api/src/main/java/org/dspace/discovery/SolrServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/discovery/SolrServiceImpl.java @@ -358,7 +358,13 @@ public void updateIndex(Context context, boolean force, String type) { final Iterator 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); + } context.uncacheEntity(indexableObject.getIndexedObject()); indexObject++; if ((indexObject % 100) == 0 && indexableObjectService instanceof ItemIndexFactory) { diff --git a/dspace-api/src/test/java/org/dspace/workflow/WorkflowCurationIT.java b/dspace-api/src/test/java/org/dspace/workflow/WorkflowCurationIT.java index 66dd2cee807f..8a07aaf7141f 100644 --- a/dspace-api/src/test/java/org/dspace/workflow/WorkflowCurationIT.java +++ b/dspace-api/src/test/java/org/dspace/workflow/WorkflowCurationIT.java @@ -22,6 +22,7 @@ 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; @@ -29,6 +30,7 @@ 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(); + //** GIVEN ** // A submitter;