From e306ca5022a4a6e73879ebcf7590eba87352c41b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 5 Nov 2025 06:47:05 +0000 Subject: [PATCH 1/4] Initial plan From 3b0a80b661529762a08e1ce990cb9aa35ea0829f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 5 Nov 2025 06:55:34 +0000 Subject: [PATCH 2/4] Fix race condition in target platform loading causing IllegalStateException - Add profile-level synchronization to prevent concurrent access to the same P2 profile - Refresh profile from registry after engine.perform() operations to maintain timestamp consistency - Prevents "Profile is not current" errors when multiple ITargetDefinition instances share the same profile Co-authored-by: laeubi <1331477+laeubi@users.noreply.github.com> --- .../internal/core/target/P2TargetUtils.java | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/target/P2TargetUtils.java b/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/target/P2TargetUtils.java index 39fd2d1ef44..b8e9036c8c4 100644 --- a/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/target/P2TargetUtils.java +++ b/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/target/P2TargetUtils.java @@ -195,6 +195,13 @@ public class P2TargetUtils { */ private static final Map SYNCHRONIZERS = new WeakHashMap<>(); + /** + * Locks for profile operations, keyed by profile ID. This ensures that operations + * on the same profile (which can be shared by multiple ITargetDefinition instances) + * are properly synchronized to prevent timestamp conflicts. + */ + private static final ConcurrentHashMap PROFILE_LOCKS = new ConcurrentHashMap<>(); + /** * Table mapping of ITargetDefinition and IFileArtifactRepository */ @@ -799,6 +806,18 @@ public static IQueryResult getIUs(ITargetDefinition target, IP return synchronizer.getProfile().query(QueryUtil.createIUAnyQuery(), null); } + /** + * Gets or creates a lock object for the given profile ID. This ensures that + * operations on the same profile (which can be shared by multiple ITargetDefinition + * instances) are properly synchronized. + * + * @param profileId the profile identifier + * @return a lock object for the profile + */ + private static Object getProfileLock(String profileId) { + return PROFILE_LOCKS.computeIfAbsent(profileId, k -> new Object()); + } + /** * Synchronize the profile and the target definition managed by this synchronizer. On return the profile will * be resolved and correctly match the given target. The IUBundleContainers associated with @@ -814,6 +833,19 @@ public static IQueryResult getIUs(ITargetDefinition target, IP * @throws CoreException if there was a problem synchronizing */ public synchronized void synchronize(ITargetDefinition target, IProgressMonitor monitor) throws CoreException { + // Additional synchronization on the profile ID to prevent race conditions when + // multiple ITargetDefinition instances (with different P2TargetUtils instances) + // share the same profile ID based on the target file path + String profileId = getProfileId(target); + synchronized (getProfileLock(profileId)) { + synchronizeInternal(target, monitor); + } + } + + /** + * Internal implementation of synchronize that runs within the profile lock. + */ + private void synchronizeInternal(ITargetDefinition target, IProgressMonitor monitor) throws CoreException { SubMonitor progress = SubMonitor.convert(monitor, 100); IProfile profile = getProfile(); // Happiness if we have a profile and it checks out or if we can load one and it checks out. @@ -845,6 +877,12 @@ public synchronized void synchronize(ITargetDefinition target, IProgressMonitor } else { resolveWithSlicer(target, profile, progress.split(60)); } + // Refresh the profile from registry as resolve operations may have updated it + profile = getProfileRegistry().getProfile(profile.getProfileId()); + if (profile == null) { + throw new CoreException( + Status.error("Profile was removed after resolving: " + getProfileId(target))); //$NON-NLS-1$ + } fProfile = profile; // If we are updating a profile then delete the old snapshot on success. notify(target, progress.split(15)); @@ -1184,6 +1222,14 @@ public IQueryable getMetadata(IProgressMonitor monitor) { throw new CoreException(result); } + // Refresh the profile from the registry after perform, as the perform operation + // creates a new profile snapshot with an updated timestamp + profile = getProfileRegistry().getProfile(profile.getProfileId()); + if (profile == null) { + throw new CoreException( + Status.error("Profile was removed after provisioning: " + getProfileId(target))); //$NON-NLS-1$ + } + // Now that we have a plan with all the binary and explicit bundles, do a second pass and add // in all the source. try { From 06efad84695c5f2f8f9effd6288b96336cee8a29 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 5 Nov 2025 06:58:55 +0000 Subject: [PATCH 3/4] Address code review feedback: extract duplicate profile refresh logic - Extract refreshProfile() helper method to reduce code duplication - Add documentation about PROFILE_LOCKS memory considerations - Improve code maintainability Co-authored-by: laeubi <1331477+laeubi@users.noreply.github.com> --- .../internal/core/target/P2TargetUtils.java | 34 +++++++++++++------ 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/target/P2TargetUtils.java b/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/target/P2TargetUtils.java index b8e9036c8c4..29bfae37686 100644 --- a/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/target/P2TargetUtils.java +++ b/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/target/P2TargetUtils.java @@ -810,6 +810,10 @@ public static IQueryResult getIUs(ITargetDefinition target, IP * Gets or creates a lock object for the given profile ID. This ensures that * operations on the same profile (which can be shared by multiple ITargetDefinition * instances) are properly synchronized. + *

+ * Note: Lock objects accumulate in memory but this is acceptable since profile IDs + * are based on stable target file paths and the lock objects are small. + *

* * @param profileId the profile identifier * @return a lock object for the profile @@ -818,6 +822,24 @@ private static Object getProfileLock(String profileId) { return PROFILE_LOCKS.computeIfAbsent(profileId, k -> new Object()); } + /** + * Refreshes a profile from the registry after operations that modify it. + * The P2 engine creates new profile snapshots with updated timestamps during + * provisioning operations, so the in-memory profile reference must be refreshed. + * + * @param profile the current profile reference (may have outdated timestamp) + * @param target the target definition + * @return the refreshed profile from the registry + * @throws CoreException if the profile was removed or cannot be loaded + */ + private IProfile refreshProfile(IProfile profile, ITargetDefinition target) throws CoreException { + IProfile refreshed = getProfileRegistry().getProfile(profile.getProfileId()); + if (refreshed == null) { + throw new CoreException(Status.error("Profile was removed: " + getProfileId(target))); //$NON-NLS-1$ + } + return refreshed; + } + /** * Synchronize the profile and the target definition managed by this synchronizer. On return the profile will * be resolved and correctly match the given target. The IUBundleContainers associated with @@ -878,11 +900,7 @@ private void synchronizeInternal(ITargetDefinition target, IProgressMonitor moni resolveWithSlicer(target, profile, progress.split(60)); } // Refresh the profile from registry as resolve operations may have updated it - profile = getProfileRegistry().getProfile(profile.getProfileId()); - if (profile == null) { - throw new CoreException( - Status.error("Profile was removed after resolving: " + getProfileId(target))); //$NON-NLS-1$ - } + profile = refreshProfile(profile, target); fProfile = profile; // If we are updating a profile then delete the old snapshot on success. notify(target, progress.split(15)); @@ -1224,11 +1242,7 @@ public IQueryable getMetadata(IProgressMonitor monitor) { // Refresh the profile from the registry after perform, as the perform operation // creates a new profile snapshot with an updated timestamp - profile = getProfileRegistry().getProfile(profile.getProfileId()); - if (profile == null) { - throw new CoreException( - Status.error("Profile was removed after provisioning: " + getProfileId(target))); //$NON-NLS-1$ - } + profile = refreshProfile(profile, target); // Now that we have a plan with all the binary and explicit bundles, do a second pass and add // in all the source. From c672ff447d5bf374eb86a8f512d1a14b96d0a242 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 5 Nov 2025 07:02:55 +0000 Subject: [PATCH 4/4] Address additional code review feedback - Enhanced documentation for PROFILE_LOCKS explaining lifecycle and memory characteristics - Clarified two-level synchronization strategy (instance + profile level) - Made profile ID retrieval consistent in refreshProfile method - Improved javadoc for better code maintainability Co-authored-by: laeubi <1331477+laeubi@users.noreply.github.com> --- .../internal/core/target/P2TargetUtils.java | 28 ++++++++++++++----- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/target/P2TargetUtils.java b/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/target/P2TargetUtils.java index 29bfae37686..1f80dc8e3eb 100644 --- a/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/target/P2TargetUtils.java +++ b/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/target/P2TargetUtils.java @@ -199,6 +199,15 @@ public class P2TargetUtils { * Locks for profile operations, keyed by profile ID. This ensures that operations * on the same profile (which can be shared by multiple ITargetDefinition instances) * are properly synchronized to prevent timestamp conflicts. + *

+ * Lock objects persist for the lifetime of the application, which is acceptable because: + *

    + *
  • Profile IDs are based on stable target file paths
  • + *
  • The number of unique target files used in a session is typically small
  • + *
  • Lock objects have minimal memory footprint
  • + *
  • Removing locks would introduce complexity and potential race conditions
  • + *
+ *

*/ private static final ConcurrentHashMap PROFILE_LOCKS = new ConcurrentHashMap<>(); @@ -828,14 +837,15 @@ private static Object getProfileLock(String profileId) { * provisioning operations, so the in-memory profile reference must be refreshed. * * @param profile the current profile reference (may have outdated timestamp) - * @param target the target definition + * @param target the target definition (used only for error messages) * @return the refreshed profile from the registry * @throws CoreException if the profile was removed or cannot be loaded */ private IProfile refreshProfile(IProfile profile, ITargetDefinition target) throws CoreException { - IProfile refreshed = getProfileRegistry().getProfile(profile.getProfileId()); + String profileId = profile.getProfileId(); + IProfile refreshed = getProfileRegistry().getProfile(profileId); if (refreshed == null) { - throw new CoreException(Status.error("Profile was removed: " + getProfileId(target))); //$NON-NLS-1$ + throw new CoreException(Status.error("Profile was removed: " + profileId)); //$NON-NLS-1$ } return refreshed; } @@ -848,9 +858,13 @@ private IProfile refreshProfile(IProfile profile, ITargetDefinition target) thro * * NOTE: this is a potentially *very* heavyweight operation. * - * NOTE: this method is synchronized as it is effectively a "test and set" caching method. Two - * threads getting the profile at the same time should not execute concurrently or the profiles - * will get out of sync. + * NOTE: this method uses two levels of synchronization: + *
    + *
  • Instance-level (synchronized method): Protects this P2TargetUtils instance's state (fProfile, etc.)
  • + *
  • Profile-level (profile ID lock): Prevents concurrent access to the same P2 profile from different + * P2TargetUtils instances, which can occur when multiple ITargetDefinition objects refer to the same + * target file and thus share a profile ID
  • + *
* * @throws CoreException if there was a problem synchronizing */ @@ -865,7 +879,7 @@ public synchronized void synchronize(ITargetDefinition target, IProgressMonitor } /** - * Internal implementation of synchronize that runs within the profile lock. + * Internal implementation of synchronize that runs within both instance and profile locks. */ private void synchronizeInternal(ITargetDefinition target, IProgressMonitor monitor) throws CoreException { SubMonitor progress = SubMonitor.convert(monitor, 100);