From b9dcda1ee8cfd4bacb16aff0e44c12978a29a34d Mon Sep 17 00:00:00 2001 From: fabrizzio-dotCMS Date: Mon, 25 May 2026 14:43:54 -0600 Subject: [PATCH 01/13] feat(opensearch) #34610: add @ESCoupled annotation and OpenSearch exception mapper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Introduce `@ESCoupled` annotation (com.dotcms.content.index) to mark ES-vendor-coupled classes for Phase 3 decommission review; searchable via reflection and grep as a structured backlog marker - Apply @ESCoupled to 8 classes: ElasticsearchStatusExceptionMapper, BulkActionListener, WorkflowAPIImpl, ESContentTool, ContentletAPI, ContentletAPIPreHook, ContentletAPIPostHook, ContentletAPIInterceptor - Add OpenSearchExceptionMapper (@Provider) as the OS counterpart of ElasticsearchStatusExceptionMapper; uses exception.status() for exact HTTP code mapping instead of string matching - Remove ReindexActionListeners inner class from DotRunnableThread (implemented ActionListener — ES vendor type) Part of #34610 Co-Authored-By: Claude Sonnet 4.6 --- .../com/dotcms/content/index/ESCoupled.java | 48 +++++++++++++++++++ .../velocity/viewtools/ESContentTool.java | 7 +++ .../ElasticsearchStatusExceptionMapper.java | 9 +++- .../mapper/OpenSearchExceptionMapper.java | 25 ++++++++++ .../common/reindex/BulkActionListener.java | 8 ++++ .../dotmarketing/db/DotRunnableThread.java | 23 --------- .../contentlet/business/ContentletAPI.java | 7 +++ .../business/ContentletAPIInterceptor.java | 7 +++ .../business/ContentletAPIPostHook.java | 9 +++- .../business/ContentletAPIPreHook.java | 9 +++- .../workflows/business/WorkflowAPIImpl.java | 8 ++++ 11 files changed, 134 insertions(+), 26 deletions(-) create mode 100644 dotCMS/src/main/java/com/dotcms/content/index/ESCoupled.java create mode 100644 dotCMS/src/main/java/com/dotcms/rest/exception/mapper/OpenSearchExceptionMapper.java diff --git a/dotCMS/src/main/java/com/dotcms/content/index/ESCoupled.java b/dotCMS/src/main/java/com/dotcms/content/index/ESCoupled.java new file mode 100644 index 000000000000..6e5e6eb7b53c --- /dev/null +++ b/dotCMS/src/main/java/com/dotcms/content/index/ESCoupled.java @@ -0,0 +1,48 @@ +package com.dotcms.content.index; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Marks a type or method as containing Elasticsearch-specific code that must be reviewed + * and decommissioned before or upon entering Phase 3 (OS-only) of the ES→OS migration. + * + *

This is the semantic counterpart of {@link IndexLibraryIndependent}. A class annotated + * with {@code @ESCoupled} either: + *

    + *
  • Implements ES-specific behaviour with no OS counterpart yet created, or
  • + *
  • Exposes ES vendor types in its public API that must be replaced with + * vendor-neutral equivalents before ES can be shut down.
  • + *
+ * + *

This annotation is informational — it does not alter runtime behaviour. + * It serves as a searchable marker during Phase 3 planning and cleanup. + * + * @see IndexLibraryIndependent + */ +@Documented +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface ESCoupled { + + /** + * Human-readable explanation of what ES-specific code exists and what + * action is required before decommission. + */ + String reason(); + + /** + * GitHub issue tracking the decommission or migration work. + * Format: "#12345" + */ + String trackedIn() default ""; + + /** + * Migration phase at which this coupling must be resolved. + * Defaults to 3 (OS-only). Set to 2 if action is required before OS reads activate. + */ + int phase() default 3; +} diff --git a/dotCMS/src/main/java/com/dotcms/rendering/velocity/viewtools/ESContentTool.java b/dotCMS/src/main/java/com/dotcms/rendering/velocity/viewtools/ESContentTool.java index 5c6bcd969228..3e14ef641230 100644 --- a/dotCMS/src/main/java/com/dotcms/rendering/velocity/viewtools/ESContentTool.java +++ b/dotCMS/src/main/java/com/dotcms/rendering/velocity/viewtools/ESContentTool.java @@ -26,8 +26,15 @@ import org.apache.velocity.tools.view.context.ViewContext; import org.apache.velocity.tools.view.tools.ViewTool; +import com.dotcms.content.index.ESCoupled; import com.liferay.portal.model.User; +@ESCoupled( + reason = "Velocity ViewTool exposes SearchResponse and ESSearchResults in deprecated bridge methods. " + + "Already delegates to neutral SearchAPI; remove bridge methods at R7 cutover.", + trackedIn = "#34610", + phase = 3 +) public class ESContentTool implements ViewTool { private HttpServletRequest req; diff --git a/dotCMS/src/main/java/com/dotcms/rest/exception/mapper/ElasticsearchStatusExceptionMapper.java b/dotCMS/src/main/java/com/dotcms/rest/exception/mapper/ElasticsearchStatusExceptionMapper.java index 85193ab752a6..ade514a8e425 100644 --- a/dotCMS/src/main/java/com/dotcms/rest/exception/mapper/ElasticsearchStatusExceptionMapper.java +++ b/dotCMS/src/main/java/com/dotcms/rest/exception/mapper/ElasticsearchStatusExceptionMapper.java @@ -1,5 +1,6 @@ package com.dotcms.rest.exception.mapper; +import com.dotcms.content.index.ESCoupled; import com.dotmarketing.util.Logger; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; @@ -7,10 +8,16 @@ import org.elasticsearch.ElasticsearchStatusException; import javax.ws.rs.ext.ExceptionMapper; +@Deprecated(forRemoval = true) @Provider +@ESCoupled( + reason = "JAX-RS mapper typed to ElasticsearchStatusException. " + + "Create a parallel OpenSearchStatusExceptionMapper — do NOT modify this class.", + trackedIn = "#34610", + phase = 3 +) public class ElasticsearchStatusExceptionMapper implements ExceptionMapper { - @Override public Response toResponse(final ElasticsearchStatusException exception) { diff --git a/dotCMS/src/main/java/com/dotcms/rest/exception/mapper/OpenSearchExceptionMapper.java b/dotCMS/src/main/java/com/dotcms/rest/exception/mapper/OpenSearchExceptionMapper.java new file mode 100644 index 000000000000..e4f286bfa9cd --- /dev/null +++ b/dotCMS/src/main/java/com/dotcms/rest/exception/mapper/OpenSearchExceptionMapper.java @@ -0,0 +1,25 @@ +package com.dotcms.rest.exception.mapper; + +import com.dotmarketing.util.Logger; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; +import org.opensearch.client.opensearch._types.OpenSearchException; + +@Provider +public class OpenSearchExceptionMapper implements ExceptionMapper { + + @Override + public Response toResponse(final OpenSearchException exception) { + + Logger.warn(this.getClass(), exception.getMessage(), exception); + + final String message = exception.error().reason(); + final String entity = ExceptionMapperUtil.getJsonErrorAsString(message); + + final Status status = Status.fromStatusCode(exception.status()); + return ExceptionMapperUtil.createResponse(entity, message, + status != null ? status : Status.INTERNAL_SERVER_ERROR); + } +} diff --git a/dotCMS/src/main/java/com/dotmarketing/common/reindex/BulkActionListener.java b/dotCMS/src/main/java/com/dotmarketing/common/reindex/BulkActionListener.java index a8ed77981a61..dac56dedc2bf 100644 --- a/dotCMS/src/main/java/com/dotmarketing/common/reindex/BulkActionListener.java +++ b/dotCMS/src/main/java/com/dotmarketing/common/reindex/BulkActionListener.java @@ -14,12 +14,20 @@ import org.elasticsearch.action.delete.DeleteResponse; import com.dotcms.business.CloseDBIfOpened; +import com.dotcms.content.index.ESCoupled; import com.dotmarketing.business.APILocator; import com.dotmarketing.business.CacheLocator; import com.dotmarketing.exception.DotDataException; import com.dotmarketing.util.Logger; import com.liferay.util.StringPool; +@Deprecated(forRemoval = true) +@ESCoupled( + reason = "Implements ActionListener and references ES bulk action types directly. " + + "Migrate to the existing vendor-neutral IndexBulkListener.", + trackedIn = "#34610", + phase = 2 +) class BulkActionListener implements ActionListener { BulkActionListener(final Map workingRecords) { diff --git a/dotCMS/src/main/java/com/dotmarketing/db/DotRunnableThread.java b/dotCMS/src/main/java/com/dotmarketing/db/DotRunnableThread.java index 03a3979f716c..b1f9298c412b 100644 --- a/dotCMS/src/main/java/com/dotmarketing/db/DotRunnableThread.java +++ b/dotCMS/src/main/java/com/dotmarketing/db/DotRunnableThread.java @@ -7,9 +7,6 @@ import com.dotmarketing.util.Config; import com.dotmarketing.util.Logger; import com.dotmarketing.util.UtilMethods; -import org.elasticsearch.action.ActionListener; -import org.elasticsearch.action.bulk.BulkResponse; - import java.util.ArrayList; import java.util.HashSet; import java.util.List; @@ -153,26 +150,6 @@ private void indexContentList(final List> reindexList, } } - private static class ReindexActionListeners implements ActionListener { - - private final List listeners; - - public ReindexActionListeners(final List listeners) { - this.listeners = listeners; - } - - @Override - public void onResponse(BulkResponse bulkItemResponses) { - listeners.stream().forEach(Runnable::run); - } - - @Override - public void onFailure(final Exception e) { - Logger.error(this, e.getMessage(), e); - } - } - - private boolean isOrdered(final Runnable runner) { return this.getOrder(runner) > 0; diff --git a/dotCMS/src/main/java/com/dotmarketing/portlets/contentlet/business/ContentletAPI.java b/dotCMS/src/main/java/com/dotmarketing/portlets/contentlet/business/ContentletAPI.java index b455b3b86581..c3c968b259a5 100644 --- a/dotCMS/src/main/java/com/dotmarketing/portlets/contentlet/business/ContentletAPI.java +++ b/dotCMS/src/main/java/com/dotmarketing/portlets/contentlet/business/ContentletAPI.java @@ -2,6 +2,7 @@ import com.dotcms.business.CloseDBIfOpened; import com.dotcms.business.WrapInTransaction; +import com.dotcms.content.index.ESCoupled; import com.dotcms.content.elasticsearch.business.ESSearchResults; import com.dotcms.content.index.IndexContentletScroll; import com.dotcms.content.elasticsearch.business.SearchCriteria; @@ -52,6 +53,12 @@ * @since Mar 22, 2012 * */ +@ESCoupled( + reason = "Public interface exposes ESSearchResults and SearchCriteria in deprecated esSearch/esSearchRaw signatures. " + + "Remove deprecated method signatures after R7 dotEvergreen cutover (~Aug 18).", + trackedIn = "#35784", + phase = 3 +) public interface ContentletAPI { /** diff --git a/dotCMS/src/main/java/com/dotmarketing/portlets/contentlet/business/ContentletAPIInterceptor.java b/dotCMS/src/main/java/com/dotmarketing/portlets/contentlet/business/ContentletAPIInterceptor.java index f9598632a049..bb071f51dbf9 100644 --- a/dotCMS/src/main/java/com/dotmarketing/portlets/contentlet/business/ContentletAPIInterceptor.java +++ b/dotCMS/src/main/java/com/dotmarketing/portlets/contentlet/business/ContentletAPIInterceptor.java @@ -1,6 +1,7 @@ package com.dotmarketing.portlets.contentlet.business; import com.dotcms.business.CloseDBIfOpened; +import com.dotcms.content.index.ESCoupled; import com.dotcms.content.index.IndexContentletScroll; import com.dotcms.content.index.domain.ContentSearchResponse; import com.dotcms.content.index.domain.ContentSearchResults; @@ -64,6 +65,12 @@ * @since 1.6.5c * */ +@ESCoupled( + reason = "Exposes org.elasticsearch.action.search.SearchResponse, ESSearchResults, and SearchCriteria. " + + "Migrate to ContentSearchResults and vendor-neutral SearchQuery.", + trackedIn = "#35784", + phase = 3 +) public class ContentletAPIInterceptor implements ContentletAPI, Interceptor { private List preHooks = new ArrayList<>(); diff --git a/dotCMS/src/main/java/com/dotmarketing/portlets/contentlet/business/ContentletAPIPostHook.java b/dotCMS/src/main/java/com/dotmarketing/portlets/contentlet/business/ContentletAPIPostHook.java index 83ddbe967a07..b20f53b730bb 100644 --- a/dotCMS/src/main/java/com/dotmarketing/portlets/contentlet/business/ContentletAPIPostHook.java +++ b/dotCMS/src/main/java/com/dotmarketing/portlets/contentlet/business/ContentletAPIPostHook.java @@ -1,5 +1,6 @@ package com.dotmarketing.portlets.contentlet.business; +import com.dotcms.content.index.ESCoupled; import com.dotcms.content.index.IndexContentletScroll; import com.dotcms.content.elasticsearch.business.SearchCriteria; import com.dotcms.contenttype.model.type.ContentType; @@ -36,9 +37,15 @@ /** * @author Jason Tesser * @since 1.6.5c - * This interface should be used as a post hook for the contentletAPI. The parameters are the same as the contentletAPI + * This interface should be used as a post hook for the contentletAPI. The parameters are the same as the contentletAPI * methods except now they also take the return type as the first parameter. */ +@ESCoupled( + reason = "Hook interface exposes SearchCriteria (ES-layer internal type) in method signatures. " + + "Migrate to vendor-neutral type when deprecated ContentletAPI signatures are removed at R7.", + trackedIn = "#35784", + phase = 3 +) public interface ContentletAPIPostHook { /** diff --git a/dotCMS/src/main/java/com/dotmarketing/portlets/contentlet/business/ContentletAPIPreHook.java b/dotCMS/src/main/java/com/dotmarketing/portlets/contentlet/business/ContentletAPIPreHook.java index 6959542bff87..fcb44c4970e7 100644 --- a/dotCMS/src/main/java/com/dotmarketing/portlets/contentlet/business/ContentletAPIPreHook.java +++ b/dotCMS/src/main/java/com/dotmarketing/portlets/contentlet/business/ContentletAPIPreHook.java @@ -1,5 +1,6 @@ package com.dotmarketing.portlets.contentlet.business; +import com.dotcms.content.index.ESCoupled; import com.dotcms.content.elasticsearch.business.SearchCriteria; import com.dotcms.contenttype.model.type.ContentType; import com.dotcms.variant.model.Variant; @@ -31,10 +32,16 @@ /** * @author Jason Tesser * @since 1.6.5c - * This interface should be used as a pre hook for the contentletAPI. If the hooks + * This interface should be used as a pre hook for the contentletAPI. If the hooks * return false then the method will throw an exception up the stack. Stopping the progress. * When possible you should always return true and let the methods go about their business. */ +@ESCoupled( + reason = "Hook interface exposes SearchCriteria (ES-layer internal type) in method signatures. " + + "Migrate to vendor-neutral type when deprecated ContentletAPI signatures are removed at R7.", + trackedIn = "#35784", + phase = 3 +) public interface ContentletAPIPreHook { /** diff --git a/dotCMS/src/main/java/com/dotmarketing/portlets/workflows/business/WorkflowAPIImpl.java b/dotCMS/src/main/java/com/dotmarketing/portlets/workflows/business/WorkflowAPIImpl.java index bbd8876e840b..8f73026e2830 100644 --- a/dotCMS/src/main/java/com/dotmarketing/portlets/workflows/business/WorkflowAPIImpl.java +++ b/dotCMS/src/main/java/com/dotmarketing/portlets/workflows/business/WorkflowAPIImpl.java @@ -176,6 +176,7 @@ import org.apache.commons.lang.time.StopWatch; import org.apache.commons.lang3.concurrent.ConcurrentUtils; import org.apache.felix.framework.OSGIUtil; +import com.dotcms.content.index.ESCoupled; import org.elasticsearch.search.query.QueryPhaseExecutionException; import org.osgi.framework.BundleContext; @@ -185,6 +186,12 @@ * @author root * @since Mar 22, 2012 */ +@ESCoupled( + reason = "Catches QueryPhaseExecutionException (org.elasticsearch.search.query) in business logic. " + + "Replace with RuntimeException or a vendor-neutral DotSearchException.", + trackedIn = "#34610", + phase = 3 +) public class WorkflowAPIImpl implements WorkflowAPI, WorkflowAPIOsgiService { private final List> actionletClasses; @@ -2769,6 +2776,7 @@ private List findContentletsToProcess(final String luceneQuery, }catch (Exception e){ final Throwable rootCause = ExceptionUtil.getRootCause(e); if(rootCause instanceof QueryPhaseExecutionException){ + final QueryPhaseExecutionException qpe = QueryPhaseExecutionException.class.cast(rootCause); Logger.debug(getClass(),()->String.format("Unable to fetch contentlets beyond an offset of %d. %s ", offset, qpe.getMessage())); } else { From 80a5c7fed1376db189c6a93dba28f3cdd52e81b4 Mon Sep 17 00:00:00 2001 From: fabrizzio-dotCMS Date: Mon, 25 May 2026 14:55:22 -0600 Subject: [PATCH 02/13] feat(opensearch) #34610: remove ES quick-win imports from vendor-neutral classes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CreateJsonWebTokenResource: org.elasticsearch.common.collect.Map → java.util.Map - HttpRequestDataUtil: remove org.elasticsearch.common.network.InetAddresses; replace two-step isInetAddress+createByteArray with single NetUtil.createByteArrayFromIpAddressString null check (Netty already on classpath, returns null for invalid addresses); also removes now-unused io.vavr.control.Try import - SearchHits (domain): remove unused org.elasticsearch.common.Nullable import - ContentsWebAPI: remove unused org.elasticsearch.search.SearchHits import and dead variable declaration SearchHits hits = null Part of #34610 Co-Authored-By: Claude Sonnet 4.6 --- .../java/com/dotcms/content/index/domain/SearchHits.java | 1 - .../rendering/velocity/viewtools/ContentsWebAPI.java | 2 -- .../api/v1/authentication/CreateJsonWebTokenResource.java | 2 +- .../src/main/java/com/dotcms/util/HttpRequestDataUtil.java | 7 ++----- 4 files changed, 3 insertions(+), 9 deletions(-) diff --git a/dotCMS/src/main/java/com/dotcms/content/index/domain/SearchHits.java b/dotCMS/src/main/java/com/dotcms/content/index/domain/SearchHits.java index 87f06c5e6648..2a4d25cff5e7 100644 --- a/dotCMS/src/main/java/com/dotcms/content/index/domain/SearchHits.java +++ b/dotCMS/src/main/java/com/dotcms/content/index/domain/SearchHits.java @@ -6,7 +6,6 @@ import java.util.Iterator; import java.util.List; import java.util.stream.Collectors; -import org.elasticsearch.common.Nullable; import org.immutables.value.Value; import org.immutables.value.Value.Default; import org.jetbrains.annotations.NotNull; diff --git a/dotCMS/src/main/java/com/dotcms/rendering/velocity/viewtools/ContentsWebAPI.java b/dotCMS/src/main/java/com/dotcms/rendering/velocity/viewtools/ContentsWebAPI.java index 6970c052a3a1..5aa5a6413f52 100644 --- a/dotCMS/src/main/java/com/dotcms/rendering/velocity/viewtools/ContentsWebAPI.java +++ b/dotCMS/src/main/java/com/dotcms/rendering/velocity/viewtools/ContentsWebAPI.java @@ -40,7 +40,6 @@ import org.apache.commons.beanutils.PropertyUtils; import org.apache.velocity.tools.view.context.ViewContext; import org.apache.velocity.tools.view.tools.ViewTool; -import org.elasticsearch.search.SearchHits; public class ContentsWebAPI implements ViewTool { @@ -870,7 +869,6 @@ public HashMap pageContent(String query, String sortBy, String perPage, String c int limit = 0; List l = new ArrayList<>(); - SearchHits hits = null; List c = new ArrayList<>(); try { diff --git a/dotCMS/src/main/java/com/dotcms/rest/api/v1/authentication/CreateJsonWebTokenResource.java b/dotCMS/src/main/java/com/dotcms/rest/api/v1/authentication/CreateJsonWebTokenResource.java index ba703f044e4c..184adfa16294 100644 --- a/dotCMS/src/main/java/com/dotcms/rest/api/v1/authentication/CreateJsonWebTokenResource.java +++ b/dotCMS/src/main/java/com/dotcms/rest/api/v1/authentication/CreateJsonWebTokenResource.java @@ -44,7 +44,7 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.ExternalDocumentation; -import org.elasticsearch.common.collect.Map; +import java.util.Map; import org.glassfish.jersey.server.JSONP; import javax.servlet.http.HttpServletRequest; diff --git a/dotCMS/src/main/java/com/dotcms/util/HttpRequestDataUtil.java b/dotCMS/src/main/java/com/dotcms/util/HttpRequestDataUtil.java index d8a57e0160d9..c3cbc52336e9 100644 --- a/dotCMS/src/main/java/com/dotcms/util/HttpRequestDataUtil.java +++ b/dotCMS/src/main/java/com/dotcms/util/HttpRequestDataUtil.java @@ -6,9 +6,7 @@ import com.dotmarketing.util.Logger; import com.dotmarketing.util.UtilMethods; import io.netty.util.NetUtil; -import io.vavr.control.Try; import org.apache.commons.lang.StringUtils; -import org.elasticsearch.common.network.InetAddresses; import javax.management.MBeanServer; import javax.management.MalformedObjectNameException; @@ -79,9 +77,8 @@ public static String getRemoteAddress (final HttpServletRequest request) { */ public static InetAddress getIpAddress(HttpServletRequest request) throws UnknownHostException { - byte[] remoteAddr = Try.of(()->InetAddresses.isInetAddress(request.getRemoteAddr())).getOrElse(false) - ? NetUtil.createByteArrayFromIpAddressString(request.getRemoteAddr()) - : new byte[]{127, 0, 0, 1}; + final byte[] parsed = NetUtil.createByteArrayFromIpAddressString(request.getRemoteAddr()); + byte[] remoteAddr = parsed != null ? parsed : new byte[]{127, 0, 0, 1}; return InetAddress.getByAddress(remoteAddr); } From 025c227ce2c89ba1e4cb64f5da6f7fcc2319d9d1 Mon Sep 17 00:00:00 2001 From: fabrizzio-dotCMS Date: Mon, 25 May 2026 15:11:26 -0600 Subject: [PATCH 03/13] =?UTF-8?q?refactor(opensearch)=20#34610:=20simplify?= =?UTF-8?q?=20@ESCoupled=20=E2=80=94=20drop=20trackedIn/phase,=20add=20rem?= =?UTF-8?q?ove[]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The annotation's purpose is Phase 3 decommission review; encoding the phase as a field was redundant. trackedIn is tracked in issue comments, not here. New shape: @ESCoupled(reason = "...", remove = {"methodA", "methodB"}) - remove[] lists specific methods/inner classes to delete at Phase 3 - omit remove[] when the entire class is to be decommissioned - Updated all 8 existing usages accordingly Co-Authored-By: Claude Sonnet 4.6 --- .../com/dotcms/content/index/ESCoupled.java | 33 ++++++------------- .../velocity/viewtools/ESContentTool.java | 5 ++- .../ElasticsearchStatusExceptionMapper.java | 4 +-- .../common/reindex/BulkActionListener.java | 4 +-- .../contentlet/business/ContentletAPI.java | 7 ++-- .../business/ContentletAPIInterceptor.java | 5 ++- .../business/ContentletAPIPostHook.java | 5 ++- .../business/ContentletAPIPreHook.java | 5 ++- .../workflows/business/WorkflowAPIImpl.java | 4 +-- 9 files changed, 24 insertions(+), 48 deletions(-) diff --git a/dotCMS/src/main/java/com/dotcms/content/index/ESCoupled.java b/dotCMS/src/main/java/com/dotcms/content/index/ESCoupled.java index 6e5e6eb7b53c..e31510b4ae49 100644 --- a/dotCMS/src/main/java/com/dotcms/content/index/ESCoupled.java +++ b/dotCMS/src/main/java/com/dotcms/content/index/ESCoupled.java @@ -7,42 +7,29 @@ import java.lang.annotation.Target; /** - * Marks a type or method as containing Elasticsearch-specific code that must be reviewed - * and decommissioned before or upon entering Phase 3 (OS-only) of the ES→OS migration. - * - *

This is the semantic counterpart of {@link IndexLibraryIndependent}. A class annotated - * with {@code @ESCoupled} either: - *

    - *
  • Implements ES-specific behaviour with no OS counterpart yet created, or
  • - *
  • Exposes ES vendor types in its public API that must be replaced with - * vendor-neutral equivalents before ES can be shut down.
  • - *
+ * Marks a type as containing Elasticsearch-specific code that must be reviewed + * and decommissioned when entering Phase 3 (OS-only) of the ES→OS migration. * *

This annotation is informational — it does not alter runtime behaviour. - * It serves as a searchable marker during Phase 3 planning and cleanup. + * Use it as a searchable marker: {@code grep -r "@ESCoupled"} produces the + * complete Phase 3 decommission backlog.

* * @see IndexLibraryIndependent */ @Documented -@Target({ElementType.TYPE, ElementType.METHOD}) +@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface ESCoupled { /** - * Human-readable explanation of what ES-specific code exists and what - * action is required before decommission. + * Why this class is coupled to Elasticsearch and what action is required + * before it can be decommissioned. */ String reason(); /** - * GitHub issue tracking the decommission or migration work. - * Format: "#12345" - */ - String trackedIn() default ""; - - /** - * Migration phase at which this coupling must be resolved. - * Defaults to 3 (OS-only). Set to 2 if action is required before OS reads activate. + * Methods or inner classes that must be removed at Phase 3. + * Leave empty when the entire class is to be decommissioned. */ - int phase() default 3; + String[] remove() default {}; } diff --git a/dotCMS/src/main/java/com/dotcms/rendering/velocity/viewtools/ESContentTool.java b/dotCMS/src/main/java/com/dotcms/rendering/velocity/viewtools/ESContentTool.java index 3e14ef641230..5512f0b64451 100644 --- a/dotCMS/src/main/java/com/dotcms/rendering/velocity/viewtools/ESContentTool.java +++ b/dotCMS/src/main/java/com/dotcms/rendering/velocity/viewtools/ESContentTool.java @@ -31,9 +31,8 @@ @ESCoupled( reason = "Velocity ViewTool exposes SearchResponse and ESSearchResults in deprecated bridge methods. " + - "Already delegates to neutral SearchAPI; remove bridge methods at R7 cutover.", - trackedIn = "#34610", - phase = 3 + "Already delegates to neutral SearchAPI — remove bridge methods at R7 cutover.", + remove = {"esSearch", "esSearchRaw"} ) public class ESContentTool implements ViewTool { diff --git a/dotCMS/src/main/java/com/dotcms/rest/exception/mapper/ElasticsearchStatusExceptionMapper.java b/dotCMS/src/main/java/com/dotcms/rest/exception/mapper/ElasticsearchStatusExceptionMapper.java index ade514a8e425..4769ab44a901 100644 --- a/dotCMS/src/main/java/com/dotcms/rest/exception/mapper/ElasticsearchStatusExceptionMapper.java +++ b/dotCMS/src/main/java/com/dotcms/rest/exception/mapper/ElasticsearchStatusExceptionMapper.java @@ -12,9 +12,7 @@ @Provider @ESCoupled( reason = "JAX-RS mapper typed to ElasticsearchStatusException. " + - "Create a parallel OpenSearchStatusExceptionMapper — do NOT modify this class.", - trackedIn = "#34610", - phase = 3 + "Decommission entire class — OpenSearchExceptionMapper is the OS replacement." ) public class ElasticsearchStatusExceptionMapper implements ExceptionMapper { diff --git a/dotCMS/src/main/java/com/dotmarketing/common/reindex/BulkActionListener.java b/dotCMS/src/main/java/com/dotmarketing/common/reindex/BulkActionListener.java index dac56dedc2bf..3059938472e0 100644 --- a/dotCMS/src/main/java/com/dotmarketing/common/reindex/BulkActionListener.java +++ b/dotCMS/src/main/java/com/dotmarketing/common/reindex/BulkActionListener.java @@ -24,9 +24,7 @@ @Deprecated(forRemoval = true) @ESCoupled( reason = "Implements ActionListener and references ES bulk action types directly. " + - "Migrate to the existing vendor-neutral IndexBulkListener.", - trackedIn = "#34610", - phase = 2 + "Decommission entire class — migrate callers to the vendor-neutral IndexBulkListener." ) class BulkActionListener implements ActionListener { diff --git a/dotCMS/src/main/java/com/dotmarketing/portlets/contentlet/business/ContentletAPI.java b/dotCMS/src/main/java/com/dotmarketing/portlets/contentlet/business/ContentletAPI.java index c3c968b259a5..bc8aebfd6e03 100644 --- a/dotCMS/src/main/java/com/dotmarketing/portlets/contentlet/business/ContentletAPI.java +++ b/dotCMS/src/main/java/com/dotmarketing/portlets/contentlet/business/ContentletAPI.java @@ -54,10 +54,9 @@ * */ @ESCoupled( - reason = "Public interface exposes ESSearchResults and SearchCriteria in deprecated esSearch/esSearchRaw signatures. " + - "Remove deprecated method signatures after R7 dotEvergreen cutover (~Aug 18).", - trackedIn = "#35784", - phase = 3 + reason = "Public interface exposes ESSearchResults and SearchCriteria in deprecated method signatures. " + + "Remove after R7 dotEvergreen cutover (~Aug 18).", + remove = {"esSearch", "esSearchRaw"} ) public interface ContentletAPI { diff --git a/dotCMS/src/main/java/com/dotmarketing/portlets/contentlet/business/ContentletAPIInterceptor.java b/dotCMS/src/main/java/com/dotmarketing/portlets/contentlet/business/ContentletAPIInterceptor.java index bb071f51dbf9..2cf0ed7101cc 100644 --- a/dotCMS/src/main/java/com/dotmarketing/portlets/contentlet/business/ContentletAPIInterceptor.java +++ b/dotCMS/src/main/java/com/dotmarketing/portlets/contentlet/business/ContentletAPIInterceptor.java @@ -67,9 +67,8 @@ */ @ESCoupled( reason = "Exposes org.elasticsearch.action.search.SearchResponse, ESSearchResults, and SearchCriteria. " + - "Migrate to ContentSearchResults and vendor-neutral SearchQuery.", - trackedIn = "#35784", - phase = 3 + "Remove ES-typed overloads and migrate to ContentSearchResults.", + remove = {"esSearch", "esSearchRaw"} ) public class ContentletAPIInterceptor implements ContentletAPI, Interceptor { diff --git a/dotCMS/src/main/java/com/dotmarketing/portlets/contentlet/business/ContentletAPIPostHook.java b/dotCMS/src/main/java/com/dotmarketing/portlets/contentlet/business/ContentletAPIPostHook.java index b20f53b730bb..9b6e1f69432a 100644 --- a/dotCMS/src/main/java/com/dotmarketing/portlets/contentlet/business/ContentletAPIPostHook.java +++ b/dotCMS/src/main/java/com/dotmarketing/portlets/contentlet/business/ContentletAPIPostHook.java @@ -42,9 +42,8 @@ */ @ESCoupled( reason = "Hook interface exposes SearchCriteria (ES-layer internal type) in method signatures. " + - "Migrate to vendor-neutral type when deprecated ContentletAPI signatures are removed at R7.", - trackedIn = "#35784", - phase = 3 + "Remove hook methods when deprecated ContentletAPI signatures are removed at R7.", + remove = {"esSearch", "esSearchRaw"} ) public interface ContentletAPIPostHook { diff --git a/dotCMS/src/main/java/com/dotmarketing/portlets/contentlet/business/ContentletAPIPreHook.java b/dotCMS/src/main/java/com/dotmarketing/portlets/contentlet/business/ContentletAPIPreHook.java index fcb44c4970e7..d1d60932531a 100644 --- a/dotCMS/src/main/java/com/dotmarketing/portlets/contentlet/business/ContentletAPIPreHook.java +++ b/dotCMS/src/main/java/com/dotmarketing/portlets/contentlet/business/ContentletAPIPreHook.java @@ -38,9 +38,8 @@ */ @ESCoupled( reason = "Hook interface exposes SearchCriteria (ES-layer internal type) in method signatures. " + - "Migrate to vendor-neutral type when deprecated ContentletAPI signatures are removed at R7.", - trackedIn = "#35784", - phase = 3 + "Remove hook methods when deprecated ContentletAPI signatures are removed at R7.", + remove = {"esSearch", "esSearchRaw"} ) public interface ContentletAPIPreHook { diff --git a/dotCMS/src/main/java/com/dotmarketing/portlets/workflows/business/WorkflowAPIImpl.java b/dotCMS/src/main/java/com/dotmarketing/portlets/workflows/business/WorkflowAPIImpl.java index 8f73026e2830..d1cc22da7bbd 100644 --- a/dotCMS/src/main/java/com/dotmarketing/portlets/workflows/business/WorkflowAPIImpl.java +++ b/dotCMS/src/main/java/com/dotmarketing/portlets/workflows/business/WorkflowAPIImpl.java @@ -188,9 +188,7 @@ */ @ESCoupled( reason = "Catches QueryPhaseExecutionException (org.elasticsearch.search.query) in business logic. " + - "Replace with RuntimeException or a vendor-neutral DotSearchException.", - trackedIn = "#34610", - phase = 3 + "Replace catch block with RuntimeException or a vendor-neutral DotSearchException." ) public class WorkflowAPIImpl implements WorkflowAPI, WorkflowAPIOsgiService { From fc6e2ff62498a506d6bdb856ba931ed2ca88d5a8 Mon Sep 17 00:00:00 2001 From: fabrizzio-dotCMS Date: Mon, 25 May 2026 15:16:33 -0600 Subject: [PATCH 04/13] feat(opensearch) #34610: remove QueryPhaseExecutionException from WorkflowAPIImpl MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace vendor-specific QueryPhaseExecutionException instanceof check with message-based isPaginationLimitReached() helper. Both ES and OS emit "Result window is too large" when a query exceeds max_result_window (10 000), so the detection is vendor-neutral. Removes the org.elasticsearch import and the @ESCoupled annotation — WorkflowAPIImpl is now ES-free. Co-Authored-By: Claude Sonnet 4.6 --- .../workflows/business/WorkflowAPIImpl.java | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/dotCMS/src/main/java/com/dotmarketing/portlets/workflows/business/WorkflowAPIImpl.java b/dotCMS/src/main/java/com/dotmarketing/portlets/workflows/business/WorkflowAPIImpl.java index d1cc22da7bbd..ef41a841519e 100644 --- a/dotCMS/src/main/java/com/dotmarketing/portlets/workflows/business/WorkflowAPIImpl.java +++ b/dotCMS/src/main/java/com/dotmarketing/portlets/workflows/business/WorkflowAPIImpl.java @@ -176,8 +176,6 @@ import org.apache.commons.lang.time.StopWatch; import org.apache.commons.lang3.concurrent.ConcurrentUtils; import org.apache.felix.framework.OSGIUtil; -import com.dotcms.content.index.ESCoupled; -import org.elasticsearch.search.query.QueryPhaseExecutionException; import org.osgi.framework.BundleContext; /** @@ -186,10 +184,6 @@ * @author root * @since Mar 22, 2012 */ -@ESCoupled( - reason = "Catches QueryPhaseExecutionException (org.elasticsearch.search.query) in business logic. " + - "Replace catch block with RuntimeException or a vendor-neutral DotSearchException." -) public class WorkflowAPIImpl implements WorkflowAPI, WorkflowAPIOsgiService { private final List> actionletClasses; @@ -2771,20 +2765,25 @@ private List findContentletsToProcess(final String luceneQuery, return ImmutableList.builder().addAll( contentletAPI.search(luceneQueryWithSteps, limit, offset, null, user, !RESPECT_FRONTEND_ROLES) ).build(); - }catch (Exception e){ + } catch (Exception e){ final Throwable rootCause = ExceptionUtil.getRootCause(e); - if(rootCause instanceof QueryPhaseExecutionException){ - - final QueryPhaseExecutionException qpe = QueryPhaseExecutionException.class.cast(rootCause); - Logger.debug(getClass(),()->String.format("Unable to fetch contentlets beyond an offset of %d. %s ", offset, qpe.getMessage())); + if (isPaginationLimitReached(rootCause)) { + Logger.debug(getClass(), () -> String.format( + "Unable to fetch contentlets beyond an offset of %d. %s", offset, rootCause.getMessage())); } else { - Logger.error(getClass(),"Unexpected Error fetching contentlets from ES", e); + Logger.error(getClass(), "Unexpected Error fetching contentlets from index", e); } } return Collections.emptyList(); } + // Both ES and OS use this message when a query exceeds the max result window (default 10 000). + private static boolean isPaginationLimitReached(final Throwable t) { + final String msg = t != null ? t.getMessage() : null; + return msg != null && msg.contains("Result window is too large"); + } + /** * Out of a given query computes the number of contentlets that will get skipped. * @param luceneQuery From 6afce8512aa67abcc4d48f85e9128acce2ae25c0 Mon Sep 17 00:00:00 2001 From: fabrizzio-dotCMS Date: Mon, 25 May 2026 16:05:57 -0600 Subject: [PATCH 05/13] fix(opensearch) #34610: guard HttpRequestDataUtil.getIpAddress against null remoteAddr NetUtil.createByteArrayFromIpAddressString throws NPE when passed null. request.getRemoteAddr() can be null in test contexts. The original code was protected by Try.of() which caught the NPE; the new code must null-check before delegating to Netty. Co-Authored-By: Claude Sonnet 4.6 --- .../src/main/java/com/dotcms/util/HttpRequestDataUtil.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/dotCMS/src/main/java/com/dotcms/util/HttpRequestDataUtil.java b/dotCMS/src/main/java/com/dotcms/util/HttpRequestDataUtil.java index c3cbc52336e9..3b0c2ddc603c 100644 --- a/dotCMS/src/main/java/com/dotcms/util/HttpRequestDataUtil.java +++ b/dotCMS/src/main/java/com/dotcms/util/HttpRequestDataUtil.java @@ -77,8 +77,9 @@ public static String getRemoteAddress (final HttpServletRequest request) { */ public static InetAddress getIpAddress(HttpServletRequest request) throws UnknownHostException { - final byte[] parsed = NetUtil.createByteArrayFromIpAddressString(request.getRemoteAddr()); - byte[] remoteAddr = parsed != null ? parsed : new byte[]{127, 0, 0, 1}; + final String addr = request.getRemoteAddr(); + final byte[] parsed = addr != null ? NetUtil.createByteArrayFromIpAddressString(addr) : null; + final byte[] remoteAddr = parsed != null ? parsed : new byte[]{127, 0, 0, 1}; return InetAddress.getByAddress(remoteAddr); } From 3bc03455fbad05c576e33a27cb1550497ecfe244 Mon Sep 17 00:00:00 2001 From: fabrizzio-dotCMS Date: Mon, 25 May 2026 16:40:33 -0600 Subject: [PATCH 06/13] refactor(telemetry): replace ByteSizeValue (ES vendor) with neutral formatBytes in TotalSizeSiteSearchIndicesMetricType Co-Authored-By: Claude Sonnet 4.6 --- .../TotalSizeSiteSearchIndicesMetricType.java | 24 +++-- ...alSizeSiteSearchIndicesMetricTypeTest.java | 92 +++++++++++++++++++ 2 files changed, 110 insertions(+), 6 deletions(-) create mode 100644 dotCMS/src/test/java/com/dotcms/telemetry/collectors/sitesearch/TotalSizeSiteSearchIndicesMetricTypeTest.java diff --git a/dotCMS/src/main/java/com/dotcms/telemetry/collectors/sitesearch/TotalSizeSiteSearchIndicesMetricType.java b/dotCMS/src/main/java/com/dotcms/telemetry/collectors/sitesearch/TotalSizeSiteSearchIndicesMetricType.java index 8fe1532cc752..19c15ec4514c 100644 --- a/dotCMS/src/main/java/com/dotcms/telemetry/collectors/sitesearch/TotalSizeSiteSearchIndicesMetricType.java +++ b/dotCMS/src/main/java/com/dotcms/telemetry/collectors/sitesearch/TotalSizeSiteSearchIndicesMetricType.java @@ -2,11 +2,9 @@ import com.dotcms.content.index.domain.IndexStats; import com.dotmarketing.exception.DotDataException; -import org.elasticsearch.common.unit.ByteSizeValue; import java.util.Collection; import java.util.Optional; -import java.util.stream.Collectors; import javax.enterprise.context.ApplicationScoped; import com.dotcms.telemetry.MetricsProfile; import com.dotcms.telemetry.ProfileType; @@ -30,9 +28,23 @@ public String getDescription() { } @Override - public Optional getValue(Collection indices) throws DotDataException { - return Optional.of(new ByteSizeValue( - indices.stream().collect(Collectors.summingLong(IndexStats::sizeRaw))).toString() - ); + public Optional getValue(final Collection indices) throws DotDataException { + final long total = indices.stream().mapToLong(IndexStats::sizeRaw).sum(); + return Optional.of(formatBytes(total)); + } + + static String formatBytes(final long bytes) { + if (bytes >= 1L << 40) return format1Decimal(bytes / (double) (1L << 40)) + "tb"; + if (bytes >= 1L << 30) return format1Decimal(bytes / (double) (1L << 30)) + "gb"; + if (bytes >= 1L << 20) return format1Decimal(bytes / (double) (1L << 20)) + "mb"; + if (bytes >= 1L << 10) return format1Decimal(bytes / (double) (1L << 10)) + "kb"; + return bytes + "b"; + } + + private static String format1Decimal(final double value) { + final String s = String.valueOf(value); + final int dot = s.indexOf('.'); + if (dot == -1) return s; + return s.substring(dot + 1).equals("0") ? s.substring(0, dot) : s.substring(0, dot + 2); } } diff --git a/dotCMS/src/test/java/com/dotcms/telemetry/collectors/sitesearch/TotalSizeSiteSearchIndicesMetricTypeTest.java b/dotCMS/src/test/java/com/dotcms/telemetry/collectors/sitesearch/TotalSizeSiteSearchIndicesMetricTypeTest.java new file mode 100644 index 000000000000..b005de410628 --- /dev/null +++ b/dotCMS/src/test/java/com/dotcms/telemetry/collectors/sitesearch/TotalSizeSiteSearchIndicesMetricTypeTest.java @@ -0,0 +1,92 @@ +package com.dotcms.telemetry.collectors.sitesearch; + +import com.dotcms.content.index.domain.ImmutableIndexStats; +import com.dotcms.content.index.domain.IndexStats; +import com.dotmarketing.exception.DotDataException; +import org.junit.Test; + +import java.util.List; +import java.util.Optional; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class TotalSizeSiteSearchIndicesMetricTypeTest { + + private final TotalSizeSiteSearchIndicesMetricType metric = new TotalSizeSiteSearchIndicesMetricType(); + + // -- metadata -- + + @Test + public void testGetName() { + assertEquals("TOTAL_INDICES_SIZE", metric.getName()); + } + + // -- formatBytes boundary cases -- + + @Test + public void testEmpty_returnsZeroBytes() throws DotDataException { + assertEquals("0b", getValue(List.of())); + } + + @Test + public void testBelowKb_returnsRawBytes() throws DotDataException { + assertEquals("512b", getValue(index(512))); + assertEquals("1023b", getValue(index(1023))); + } + + @Test + public void testExactKb_dropsDecimal() throws DotDataException { + assertEquals("1kb", getValue(index(1024))); + } + + @Test + public void testFractionalKb_keepsOneDecimal() throws DotDataException { + assertEquals("1.5kb", getValue(index(1536))); + } + + @Test + public void testExactMb_dropsDecimal() throws DotDataException { + assertEquals("1mb", getValue(index(1024L * 1024))); + } + + @Test + public void testFractionalGb_keepsOneDecimal() throws DotDataException { + assertEquals("1.5gb", getValue(index((long) (1.5 * 1024 * 1024 * 1024)))); + } + + @Test + public void testLargeMb_noDecimal() throws DotDataException { + assertEquals("128mb", getValue(index(128L * 1024 * 1024))); + } + + // -- aggregation across multiple indices -- + + @Test + public void testMultipleIndices_sumsCorrectly() throws DotDataException { + final IndexStats a = index(512L * 1024 * 1024); // 512 MB + final IndexStats b = index(512L * 1024 * 1024); // 512 MB → total 1 GB + assertEquals("1gb", getValue(a, b)); + } + + // -- helpers -- + + private String getValue(final IndexStats... indices) throws DotDataException { + return getValue(List.of(indices)); + } + + private String getValue(final List indices) throws DotDataException { + final Optional result = metric.getValue(indices); + assertTrue(result.isPresent()); + return (String) result.get(); + } + + private static IndexStats index(final long sizeRaw) { + return ImmutableIndexStats.builder() + .indexName("test") + .documentCount(0) + .sizeRaw(sizeRaw) + .size("") + .build(); + } +} From 48ef818eb3e209072e232c3e51b34749dfeaaf53 Mon Sep 17 00:00:00 2001 From: fabrizzio-dotCMS Date: Mon, 25 May 2026 16:46:51 -0600 Subject: [PATCH 07/13] docs(telemetry): add Javadoc to TotalSizeSiteSearchIndicesMetricType and formatBytes Co-Authored-By: Claude Sonnet 4.6 --- .../TotalSizeSiteSearchIndicesMetricType.java | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/dotCMS/src/main/java/com/dotcms/telemetry/collectors/sitesearch/TotalSizeSiteSearchIndicesMetricType.java b/dotCMS/src/main/java/com/dotcms/telemetry/collectors/sitesearch/TotalSizeSiteSearchIndicesMetricType.java index 19c15ec4514c..3cb37b4c282e 100644 --- a/dotCMS/src/main/java/com/dotcms/telemetry/collectors/sitesearch/TotalSizeSiteSearchIndicesMetricType.java +++ b/dotCMS/src/main/java/com/dotcms/telemetry/collectors/sitesearch/TotalSizeSiteSearchIndicesMetricType.java @@ -10,13 +10,17 @@ import com.dotcms.telemetry.ProfileType; /** - * Collect the total size in Mb of all the Site Search indices. + * Telemetry metric that reports the combined storage size of all Site Search indices. + * + *

The value is returned as a human-readable string with the largest applicable unit + * (b / kb / mb / gb / tb) and at most one decimal place — for example {@code "1.5gb"} or + * {@code "128mb"}. Trailing {@code .0} decimals are dropped so exact multiples render + * without noise (e.g. {@code "1gb"}, not {@code "1.0gb"}). */ @MetricsProfile(ProfileType.FULL) @ApplicationScoped public class TotalSizeSiteSearchIndicesMetricType extends IndicesSiteSearchMetricType { - @Override public String getName() { return "TOTAL_INDICES_SIZE"; @@ -33,6 +37,10 @@ public Optional getValue(final Collection indices) throws Do return Optional.of(formatBytes(total)); } + /** + * Formats a byte count as a human-readable string using binary units (1 kb = 1024 b). + * At most one decimal place is shown; trailing {@code .0} is omitted. + */ static String formatBytes(final long bytes) { if (bytes >= 1L << 40) return format1Decimal(bytes / (double) (1L << 40)) + "tb"; if (bytes >= 1L << 30) return format1Decimal(bytes / (double) (1L << 30)) + "gb"; From 9727e03b97c29a15bb4e9ed8e25069b0868bfa2e Mon Sep 17 00:00:00 2001 From: fabrizzio-dotCMS Date: Mon, 25 May 2026 17:16:34 -0600 Subject: [PATCH 08/13] revert(opensearch) #34610: restore WorkflowAPIImpl to pre-branch state MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Exclude WorkflowAPIImpl from this PR — the QueryPhaseExecutionException replacement will be handled in a dedicated PR that introduces a neutral DotIndexWindowLimitException thrown by the factory layer. Co-Authored-By: Claude Sonnet 4.6 --- .../workflows/business/WorkflowAPIImpl.java | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/dotCMS/src/main/java/com/dotmarketing/portlets/workflows/business/WorkflowAPIImpl.java b/dotCMS/src/main/java/com/dotmarketing/portlets/workflows/business/WorkflowAPIImpl.java index ef41a841519e..d1cc22da7bbd 100644 --- a/dotCMS/src/main/java/com/dotmarketing/portlets/workflows/business/WorkflowAPIImpl.java +++ b/dotCMS/src/main/java/com/dotmarketing/portlets/workflows/business/WorkflowAPIImpl.java @@ -176,6 +176,8 @@ import org.apache.commons.lang.time.StopWatch; import org.apache.commons.lang3.concurrent.ConcurrentUtils; import org.apache.felix.framework.OSGIUtil; +import com.dotcms.content.index.ESCoupled; +import org.elasticsearch.search.query.QueryPhaseExecutionException; import org.osgi.framework.BundleContext; /** @@ -184,6 +186,10 @@ * @author root * @since Mar 22, 2012 */ +@ESCoupled( + reason = "Catches QueryPhaseExecutionException (org.elasticsearch.search.query) in business logic. " + + "Replace catch block with RuntimeException or a vendor-neutral DotSearchException." +) public class WorkflowAPIImpl implements WorkflowAPI, WorkflowAPIOsgiService { private final List> actionletClasses; @@ -2765,25 +2771,20 @@ private List findContentletsToProcess(final String luceneQuery, return ImmutableList.builder().addAll( contentletAPI.search(luceneQueryWithSteps, limit, offset, null, user, !RESPECT_FRONTEND_ROLES) ).build(); - } catch (Exception e){ + }catch (Exception e){ final Throwable rootCause = ExceptionUtil.getRootCause(e); - if (isPaginationLimitReached(rootCause)) { - Logger.debug(getClass(), () -> String.format( - "Unable to fetch contentlets beyond an offset of %d. %s", offset, rootCause.getMessage())); + if(rootCause instanceof QueryPhaseExecutionException){ + + final QueryPhaseExecutionException qpe = QueryPhaseExecutionException.class.cast(rootCause); + Logger.debug(getClass(),()->String.format("Unable to fetch contentlets beyond an offset of %d. %s ", offset, qpe.getMessage())); } else { - Logger.error(getClass(), "Unexpected Error fetching contentlets from index", e); + Logger.error(getClass(),"Unexpected Error fetching contentlets from ES", e); } } return Collections.emptyList(); } - // Both ES and OS use this message when a query exceeds the max result window (default 10 000). - private static boolean isPaginationLimitReached(final Throwable t) { - final String msg = t != null ? t.getMessage() : null; - return msg != null && msg.contains("Result window is too large"); - } - /** * Out of a given query computes the number of contentlets that will get skipped. * @param luceneQuery From f41606193e13fd6ce9a434776b4a850d7cbf136b Mon Sep 17 00:00:00 2001 From: fabrizzio-dotCMS Date: Mon, 25 May 2026 17:35:05 -0600 Subject: [PATCH 09/13] revert(opensearch): restore WorkflowAPIImpl to main state MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove @ESCoupled annotation and extra blank line — WorkflowAPIImpl is excluded from this PR. Vendor-neutral exception handling tracked separately in #35827. Co-Authored-By: Claude Sonnet 4.6 --- .../portlets/workflows/business/WorkflowAPIImpl.java | 6 ------ 1 file changed, 6 deletions(-) diff --git a/dotCMS/src/main/java/com/dotmarketing/portlets/workflows/business/WorkflowAPIImpl.java b/dotCMS/src/main/java/com/dotmarketing/portlets/workflows/business/WorkflowAPIImpl.java index d1cc22da7bbd..bbd8876e840b 100644 --- a/dotCMS/src/main/java/com/dotmarketing/portlets/workflows/business/WorkflowAPIImpl.java +++ b/dotCMS/src/main/java/com/dotmarketing/portlets/workflows/business/WorkflowAPIImpl.java @@ -176,7 +176,6 @@ import org.apache.commons.lang.time.StopWatch; import org.apache.commons.lang3.concurrent.ConcurrentUtils; import org.apache.felix.framework.OSGIUtil; -import com.dotcms.content.index.ESCoupled; import org.elasticsearch.search.query.QueryPhaseExecutionException; import org.osgi.framework.BundleContext; @@ -186,10 +185,6 @@ * @author root * @since Mar 22, 2012 */ -@ESCoupled( - reason = "Catches QueryPhaseExecutionException (org.elasticsearch.search.query) in business logic. " + - "Replace catch block with RuntimeException or a vendor-neutral DotSearchException." -) public class WorkflowAPIImpl implements WorkflowAPI, WorkflowAPIOsgiService { private final List> actionletClasses; @@ -2774,7 +2769,6 @@ private List findContentletsToProcess(final String luceneQuery, }catch (Exception e){ final Throwable rootCause = ExceptionUtil.getRootCause(e); if(rootCause instanceof QueryPhaseExecutionException){ - final QueryPhaseExecutionException qpe = QueryPhaseExecutionException.class.cast(rootCause); Logger.debug(getClass(),()->String.format("Unable to fetch contentlets beyond an offset of %d. %s ", offset, qpe.getMessage())); } else { From 43438bebebe023ef7ff192a3f23dac98ec4e0051 Mon Sep 17 00:00:00 2001 From: fabrizzio-dotCMS Date: Tue, 26 May 2026 08:42:50 -0600 Subject: [PATCH 10/13] fix(opensearch) #34610: address PR #35824 review feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Guard exception.error().reason() with Optional to avoid NPE when OpenSearchException carries no server-side error body (e.g. transport errors) - Add testTruncationNotRounding to document that formatBytes truncates (string slice), not rounds: 1.99 GB → "1.9gb" is intentional Co-Authored-By: Claude Sonnet 4.6 --- .../rest/exception/mapper/OpenSearchExceptionMapper.java | 6 +++++- .../TotalSizeSiteSearchIndicesMetricTypeTest.java | 8 ++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/dotCMS/src/main/java/com/dotcms/rest/exception/mapper/OpenSearchExceptionMapper.java b/dotCMS/src/main/java/com/dotcms/rest/exception/mapper/OpenSearchExceptionMapper.java index e4f286bfa9cd..78b42d80a8f7 100644 --- a/dotCMS/src/main/java/com/dotcms/rest/exception/mapper/OpenSearchExceptionMapper.java +++ b/dotCMS/src/main/java/com/dotcms/rest/exception/mapper/OpenSearchExceptionMapper.java @@ -1,10 +1,12 @@ package com.dotcms.rest.exception.mapper; import com.dotmarketing.util.Logger; +import java.util.Optional; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; import javax.ws.rs.ext.ExceptionMapper; import javax.ws.rs.ext.Provider; +import org.opensearch.client.opensearch._types.ErrorCause; import org.opensearch.client.opensearch._types.OpenSearchException; @Provider @@ -15,7 +17,9 @@ public Response toResponse(final OpenSearchException exception) { Logger.warn(this.getClass(), exception.getMessage(), exception); - final String message = exception.error().reason(); + final String message = Optional.ofNullable(exception.error()) + .map(ErrorCause::reason) + .orElse(exception.getMessage()); final String entity = ExceptionMapperUtil.getJsonErrorAsString(message); final Status status = Status.fromStatusCode(exception.status()); diff --git a/dotCMS/src/test/java/com/dotcms/telemetry/collectors/sitesearch/TotalSizeSiteSearchIndicesMetricTypeTest.java b/dotCMS/src/test/java/com/dotcms/telemetry/collectors/sitesearch/TotalSizeSiteSearchIndicesMetricTypeTest.java index b005de410628..00baaf4cb173 100644 --- a/dotCMS/src/test/java/com/dotcms/telemetry/collectors/sitesearch/TotalSizeSiteSearchIndicesMetricTypeTest.java +++ b/dotCMS/src/test/java/com/dotcms/telemetry/collectors/sitesearch/TotalSizeSiteSearchIndicesMetricTypeTest.java @@ -60,6 +60,14 @@ public void testLargeMb_noDecimal() throws DotDataException { assertEquals("128mb", getValue(index(128L * 1024 * 1024))); } + @Test + public void testTruncationNotRounding() throws DotDataException { + // format1Decimal truncates (string slice), it does not round. + // 1.99 GB truncates to "1.9gb", not "2.0gb". + final long GB = 1L << 30; + assertEquals("1.9gb", getValue(index((long) (1.99 * GB)))); + } + // -- aggregation across multiple indices -- @Test From aa6cc7b23d9bcf1e1e03f582543fd9788ac6ae51 Mon Sep 17 00:00:00 2001 From: fabrizzio-dotCMS Date: Wed, 27 May 2026 10:11:36 -0600 Subject: [PATCH 11/13] refactor(opensearch) #34610: replace @ESCoupled annotation with ES-DECOMMISSION comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove ESCoupled.java annotation class and replace all usages with inline // ES-DECOMMISSION: comments that preserve the decommission context and remain greppable via: grep -r "ES-DECOMMISSION" Affected classes (7): - ESContentTool — bridge methods esSearch/esSearchRaw - ElasticsearchStatusExceptionMapper — full class, replaced by OpenSearchExceptionMapper - BulkActionListener — full class, replaced by IndexBulkListener - ContentletAPI — deprecated method signatures esSearch/esSearchRaw - ContentletAPIInterceptor — same as ContentletAPI - ContentletAPIPostHook — hook methods for deprecated signatures - ContentletAPIPreHook — hook methods for deprecated signatures Co-Authored-By: Claude Sonnet 4.6 --- .../com/dotcms/content/index/ESCoupled.java | 35 ------------------- .../velocity/viewtools/ESContentTool.java | 8 ++--- .../ElasticsearchStatusExceptionMapper.java | 7 ++-- .../common/reindex/BulkActionListener.java | 7 ++-- .../contentlet/business/ContentletAPI.java | 8 ++--- .../business/ContentletAPIInterceptor.java | 8 ++--- .../business/ContentletAPIPostHook.java | 8 ++--- .../business/ContentletAPIPreHook.java | 8 ++--- 8 files changed, 14 insertions(+), 75 deletions(-) delete mode 100644 dotCMS/src/main/java/com/dotcms/content/index/ESCoupled.java diff --git a/dotCMS/src/main/java/com/dotcms/content/index/ESCoupled.java b/dotCMS/src/main/java/com/dotcms/content/index/ESCoupled.java deleted file mode 100644 index e31510b4ae49..000000000000 --- a/dotCMS/src/main/java/com/dotcms/content/index/ESCoupled.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.dotcms.content.index; - -import java.lang.annotation.Documented; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * Marks a type as containing Elasticsearch-specific code that must be reviewed - * and decommissioned when entering Phase 3 (OS-only) of the ES→OS migration. - * - *

This annotation is informational — it does not alter runtime behaviour. - * Use it as a searchable marker: {@code grep -r "@ESCoupled"} produces the - * complete Phase 3 decommission backlog.

- * - * @see IndexLibraryIndependent - */ -@Documented -@Target(ElementType.TYPE) -@Retention(RetentionPolicy.RUNTIME) -public @interface ESCoupled { - - /** - * Why this class is coupled to Elasticsearch and what action is required - * before it can be decommissioned. - */ - String reason(); - - /** - * Methods or inner classes that must be removed at Phase 3. - * Leave empty when the entire class is to be decommissioned. - */ - String[] remove() default {}; -} diff --git a/dotCMS/src/main/java/com/dotcms/rendering/velocity/viewtools/ESContentTool.java b/dotCMS/src/main/java/com/dotcms/rendering/velocity/viewtools/ESContentTool.java index 5512f0b64451..1f9c3f9d28d6 100644 --- a/dotCMS/src/main/java/com/dotcms/rendering/velocity/viewtools/ESContentTool.java +++ b/dotCMS/src/main/java/com/dotcms/rendering/velocity/viewtools/ESContentTool.java @@ -26,14 +26,10 @@ import org.apache.velocity.tools.view.context.ViewContext; import org.apache.velocity.tools.view.tools.ViewTool; -import com.dotcms.content.index.ESCoupled; import com.liferay.portal.model.User; -@ESCoupled( - reason = "Velocity ViewTool exposes SearchResponse and ESSearchResults in deprecated bridge methods. " + - "Already delegates to neutral SearchAPI — remove bridge methods at R7 cutover.", - remove = {"esSearch", "esSearchRaw"} -) +// ES-DECOMMISSION: Velocity ViewTool exposes SearchResponse and ESSearchResults in deprecated +// bridge methods. Already delegates to neutral SearchAPI — remove esSearch, esSearchRaw at R7 cutover. public class ESContentTool implements ViewTool { private HttpServletRequest req; diff --git a/dotCMS/src/main/java/com/dotcms/rest/exception/mapper/ElasticsearchStatusExceptionMapper.java b/dotCMS/src/main/java/com/dotcms/rest/exception/mapper/ElasticsearchStatusExceptionMapper.java index 4769ab44a901..bac1f6e812fb 100644 --- a/dotCMS/src/main/java/com/dotcms/rest/exception/mapper/ElasticsearchStatusExceptionMapper.java +++ b/dotCMS/src/main/java/com/dotcms/rest/exception/mapper/ElasticsearchStatusExceptionMapper.java @@ -1,6 +1,5 @@ package com.dotcms.rest.exception.mapper; -import com.dotcms.content.index.ESCoupled; import com.dotmarketing.util.Logger; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; @@ -8,12 +7,10 @@ import org.elasticsearch.ElasticsearchStatusException; import javax.ws.rs.ext.ExceptionMapper; +// ES-DECOMMISSION: JAX-RS mapper typed to ElasticsearchStatusException. +// Decommission entire class — OpenSearchExceptionMapper is the OS replacement. @Deprecated(forRemoval = true) @Provider -@ESCoupled( - reason = "JAX-RS mapper typed to ElasticsearchStatusException. " + - "Decommission entire class — OpenSearchExceptionMapper is the OS replacement." -) public class ElasticsearchStatusExceptionMapper implements ExceptionMapper { @Override diff --git a/dotCMS/src/main/java/com/dotmarketing/common/reindex/BulkActionListener.java b/dotCMS/src/main/java/com/dotmarketing/common/reindex/BulkActionListener.java index 3059938472e0..a3ebc4471766 100644 --- a/dotCMS/src/main/java/com/dotmarketing/common/reindex/BulkActionListener.java +++ b/dotCMS/src/main/java/com/dotmarketing/common/reindex/BulkActionListener.java @@ -14,18 +14,15 @@ import org.elasticsearch.action.delete.DeleteResponse; import com.dotcms.business.CloseDBIfOpened; -import com.dotcms.content.index.ESCoupled; import com.dotmarketing.business.APILocator; import com.dotmarketing.business.CacheLocator; import com.dotmarketing.exception.DotDataException; import com.dotmarketing.util.Logger; import com.liferay.util.StringPool; +// ES-DECOMMISSION: Implements ActionListener and references ES bulk action types directly. +// Decommission entire class — migrate callers to the vendor-neutral IndexBulkListener. @Deprecated(forRemoval = true) -@ESCoupled( - reason = "Implements ActionListener and references ES bulk action types directly. " + - "Decommission entire class — migrate callers to the vendor-neutral IndexBulkListener." -) class BulkActionListener implements ActionListener { BulkActionListener(final Map workingRecords) { diff --git a/dotCMS/src/main/java/com/dotmarketing/portlets/contentlet/business/ContentletAPI.java b/dotCMS/src/main/java/com/dotmarketing/portlets/contentlet/business/ContentletAPI.java index bc8aebfd6e03..648336ce3493 100644 --- a/dotCMS/src/main/java/com/dotmarketing/portlets/contentlet/business/ContentletAPI.java +++ b/dotCMS/src/main/java/com/dotmarketing/portlets/contentlet/business/ContentletAPI.java @@ -2,7 +2,6 @@ import com.dotcms.business.CloseDBIfOpened; import com.dotcms.business.WrapInTransaction; -import com.dotcms.content.index.ESCoupled; import com.dotcms.content.elasticsearch.business.ESSearchResults; import com.dotcms.content.index.IndexContentletScroll; import com.dotcms.content.elasticsearch.business.SearchCriteria; @@ -53,11 +52,8 @@ * @since Mar 22, 2012 * */ -@ESCoupled( - reason = "Public interface exposes ESSearchResults and SearchCriteria in deprecated method signatures. " + - "Remove after R7 dotEvergreen cutover (~Aug 18).", - remove = {"esSearch", "esSearchRaw"} -) +// ES-DECOMMISSION: Public interface exposes ESSearchResults and SearchCriteria in deprecated method +// signatures. Remove esSearch, esSearchRaw after R7 dotEvergreen cutover (~Aug 18). public interface ContentletAPI { /** diff --git a/dotCMS/src/main/java/com/dotmarketing/portlets/contentlet/business/ContentletAPIInterceptor.java b/dotCMS/src/main/java/com/dotmarketing/portlets/contentlet/business/ContentletAPIInterceptor.java index 2cf0ed7101cc..88b8f574abdc 100644 --- a/dotCMS/src/main/java/com/dotmarketing/portlets/contentlet/business/ContentletAPIInterceptor.java +++ b/dotCMS/src/main/java/com/dotmarketing/portlets/contentlet/business/ContentletAPIInterceptor.java @@ -1,7 +1,6 @@ package com.dotmarketing.portlets.contentlet.business; import com.dotcms.business.CloseDBIfOpened; -import com.dotcms.content.index.ESCoupled; import com.dotcms.content.index.IndexContentletScroll; import com.dotcms.content.index.domain.ContentSearchResponse; import com.dotcms.content.index.domain.ContentSearchResults; @@ -65,11 +64,8 @@ * @since 1.6.5c * */ -@ESCoupled( - reason = "Exposes org.elasticsearch.action.search.SearchResponse, ESSearchResults, and SearchCriteria. " + - "Remove ES-typed overloads and migrate to ContentSearchResults.", - remove = {"esSearch", "esSearchRaw"} -) +// ES-DECOMMISSION: Exposes org.elasticsearch.action.search.SearchResponse, ESSearchResults, and SearchCriteria. +// Remove esSearch, esSearchRaw — migrate to ContentSearchResults. public class ContentletAPIInterceptor implements ContentletAPI, Interceptor { private List preHooks = new ArrayList<>(); diff --git a/dotCMS/src/main/java/com/dotmarketing/portlets/contentlet/business/ContentletAPIPostHook.java b/dotCMS/src/main/java/com/dotmarketing/portlets/contentlet/business/ContentletAPIPostHook.java index 9b6e1f69432a..1f80dfe1960e 100644 --- a/dotCMS/src/main/java/com/dotmarketing/portlets/contentlet/business/ContentletAPIPostHook.java +++ b/dotCMS/src/main/java/com/dotmarketing/portlets/contentlet/business/ContentletAPIPostHook.java @@ -1,6 +1,5 @@ package com.dotmarketing.portlets.contentlet.business; -import com.dotcms.content.index.ESCoupled; import com.dotcms.content.index.IndexContentletScroll; import com.dotcms.content.elasticsearch.business.SearchCriteria; import com.dotcms.contenttype.model.type.ContentType; @@ -40,11 +39,8 @@ * This interface should be used as a post hook for the contentletAPI. The parameters are the same as the contentletAPI * methods except now they also take the return type as the first parameter. */ -@ESCoupled( - reason = "Hook interface exposes SearchCriteria (ES-layer internal type) in method signatures. " + - "Remove hook methods when deprecated ContentletAPI signatures are removed at R7.", - remove = {"esSearch", "esSearchRaw"} -) +// ES-DECOMMISSION: Hook interface exposes SearchCriteria (ES-layer internal type) in method signatures. +// Remove esSearch, esSearchRaw hook methods when deprecated ContentletAPI signatures are removed at R7. public interface ContentletAPIPostHook { /** diff --git a/dotCMS/src/main/java/com/dotmarketing/portlets/contentlet/business/ContentletAPIPreHook.java b/dotCMS/src/main/java/com/dotmarketing/portlets/contentlet/business/ContentletAPIPreHook.java index d1d60932531a..b6febb9c08ba 100644 --- a/dotCMS/src/main/java/com/dotmarketing/portlets/contentlet/business/ContentletAPIPreHook.java +++ b/dotCMS/src/main/java/com/dotmarketing/portlets/contentlet/business/ContentletAPIPreHook.java @@ -1,6 +1,5 @@ package com.dotmarketing.portlets.contentlet.business; -import com.dotcms.content.index.ESCoupled; import com.dotcms.content.elasticsearch.business.SearchCriteria; import com.dotcms.contenttype.model.type.ContentType; import com.dotcms.variant.model.Variant; @@ -36,11 +35,8 @@ * return false then the method will throw an exception up the stack. Stopping the progress. * When possible you should always return true and let the methods go about their business. */ -@ESCoupled( - reason = "Hook interface exposes SearchCriteria (ES-layer internal type) in method signatures. " + - "Remove hook methods when deprecated ContentletAPI signatures are removed at R7.", - remove = {"esSearch", "esSearchRaw"} -) +// ES-DECOMMISSION: Hook interface exposes SearchCriteria (ES-layer internal type) in method signatures. +// Remove esSearch, esSearchRaw hook methods when deprecated ContentletAPI signatures are removed at R7. public interface ContentletAPIPreHook { /** From eec5e597e473177fb2f33712844c111d62cfe8f8 Mon Sep 17 00:00:00 2001 From: fabrizzio-dotCMS Date: Wed, 27 May 2026 10:23:43 -0600 Subject: [PATCH 12/13] docs(opensearch) #34610: document deferred ES migration in WorkflowAPIImpl MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add ES-DECOMMISSION comment explaining why QueryPhaseExecutionException was not migrated to OS 3.x in this branch: no typed equivalent exists in the OpenSearch Java client — detection at this call-site would require message parsing. Deferred to Phase 3 alongside ContentletAPI cleanup. Co-Authored-By: Claude Sonnet 4.6 --- .../portlets/workflows/business/WorkflowAPIImpl.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/dotCMS/src/main/java/com/dotmarketing/portlets/workflows/business/WorkflowAPIImpl.java b/dotCMS/src/main/java/com/dotmarketing/portlets/workflows/business/WorkflowAPIImpl.java index bbd8876e840b..0c7984b7bda3 100644 --- a/dotCMS/src/main/java/com/dotmarketing/portlets/workflows/business/WorkflowAPIImpl.java +++ b/dotCMS/src/main/java/com/dotmarketing/portlets/workflows/business/WorkflowAPIImpl.java @@ -2768,6 +2768,12 @@ private List findContentletsToProcess(final String luceneQuery, ).build(); }catch (Exception e){ final Throwable rootCause = ExceptionUtil.getRootCause(e); + // ES-DECOMMISSION (Phase 3): Migration to OpenSearch was evaluated here. + // No typed equivalent of QueryPhaseExecutionException exists in the OpenSearch Java client 3.x — + // the window-limit condition surfaces as OpenSearchException with a structured error body, + // but detecting it at this call-site would require parsing error messages, which is fragile. + // Decision: keep this ES-specific check as-is until Phase 3, when the ES layer is removed + // and the catch block is replaced or dropped entirely alongside ContentletAPI cleanup. if(rootCause instanceof QueryPhaseExecutionException){ final QueryPhaseExecutionException qpe = QueryPhaseExecutionException.class.cast(rootCause); Logger.debug(getClass(),()->String.format("Unable to fetch contentlets beyond an offset of %d. %s ", offset, qpe.getMessage())); From 43251f15b4d2f4ece368320d38b28a2a8909a2a9 Mon Sep 17 00:00:00 2001 From: fabrizzio-dotCMS Date: Wed, 27 May 2026 10:37:27 -0600 Subject: [PATCH 13/13] =?UTF-8?q?refactor(opensearch)=20#34610:=20simplify?= =?UTF-8?q?=20WorkflowAPIImpl=20catch=20=E2=80=94=20remove=20ES-specific?= =?UTF-8?q?=20QPE=20branch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the if/else that branched on QueryPhaseExecutionException with a single generic Logger.warnAndDebug call covering both the window-limit and any other unexpected search failure. Reason: the QPE branch never fires via the REST client (which wraps all server errors as ElasticsearchStatusException), and no typed OS equivalent exists in OpenSearch Java client 3.x. A comment documents the decision to defer full vendor-neutral handling to Phase 3 at the factory layer. Also removes the now-unused QPE import. Co-Authored-By: Claude Sonnet 4.6 --- .../workflows/business/WorkflowAPIImpl.java | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/dotCMS/src/main/java/com/dotmarketing/portlets/workflows/business/WorkflowAPIImpl.java b/dotCMS/src/main/java/com/dotmarketing/portlets/workflows/business/WorkflowAPIImpl.java index 0c7984b7bda3..24d179703377 100644 --- a/dotCMS/src/main/java/com/dotmarketing/portlets/workflows/business/WorkflowAPIImpl.java +++ b/dotCMS/src/main/java/com/dotmarketing/portlets/workflows/business/WorkflowAPIImpl.java @@ -176,7 +176,6 @@ import org.apache.commons.lang.time.StopWatch; import org.apache.commons.lang3.concurrent.ConcurrentUtils; import org.apache.felix.framework.OSGIUtil; -import org.elasticsearch.search.query.QueryPhaseExecutionException; import org.osgi.framework.BundleContext; /** @@ -2767,19 +2766,16 @@ private List findContentletsToProcess(final String luceneQuery, contentletAPI.search(luceneQueryWithSteps, limit, offset, null, user, !RESPECT_FRONTEND_ROLES) ).build(); }catch (Exception e){ - final Throwable rootCause = ExceptionUtil.getRootCause(e); - // ES-DECOMMISSION (Phase 3): Migration to OpenSearch was evaluated here. - // No typed equivalent of QueryPhaseExecutionException exists in the OpenSearch Java client 3.x — - // the window-limit condition surfaces as OpenSearchException with a structured error body, - // but detecting it at this call-site would require parsing error messages, which is fragile. - // Decision: keep this ES-specific check as-is until Phase 3, when the ES layer is removed - // and the catch block is replaced or dropped entirely alongside ContentletAPI cleanup. - if(rootCause instanceof QueryPhaseExecutionException){ - final QueryPhaseExecutionException qpe = QueryPhaseExecutionException.class.cast(rootCause); - Logger.debug(getClass(),()->String.format("Unable to fetch contentlets beyond an offset of %d. %s ", offset, qpe.getMessage())); - } else { - Logger.error(getClass(),"Unexpected Error fetching contentlets from ES", e); - } + // A single generic message covers both the window-limit case (offset > max_result_window) + // and any other unexpected search failure. The ES-specific QueryPhaseExecutionException + // branch was removed because: (a) it never fires via the REST client — the client wraps + // all server errors as ElasticsearchStatusException — and (b) no typed OS equivalent exists + // in OpenSearch Java client 3.x. Detection at this call-site would require fragile message + // parsing. Full vendor-neutral handling belongs at the factory layer (Phase 3). + Logger.warnAndDebug(getClass(), + String.format("Unexpected error fetching contentlets at offset=%d — " + + "possibly an index window-limit exceeded if offset surpasses max_result_window. %s", + offset, e.getMessage()), e); } return Collections.emptyList();