Skip to content

Add migration-phase / store control to ContentletDataGen for ES→OS dual-write tests #36266

Description

@fabrizzio-dotCMS

Description

Tests cannot currently control which search store (ElasticSearch, OpenSearch, or both) a contentlet is indexed into when created through ContentletDataGen. This blocks straightforward verification of the ES→OS dual-write pipeline from the data-generation layer.

Current state (confirmed in code)

  • Indexing seam: ContentletDataGen.persist()checkin(contentlet, policy) (dotcms-integration/src/test/java/com/dotcms/datagen/ContentletDataGen.java:227).
  • ContentletDataGen.setPolicy(IndexPolicy) controls when indexing happens (FORCE / WAIT_FOR / NONE) — but not which store receives the write.
  • The migration phase is global only: tests flip Config.setProperty("FEATURE_FLAG_OPEN_SEARCH_PHASE", "1") by hand.
  • Routing lives in PhaseRouter (ContentletIndexAPIImpl.java:173); phase → write-providers is 0:[ES] 1:[ES,OS] 2:[ES,OS] 3:[OS].
  • The existing migration ITs (ContentletIndexAPIImplMigrationIntegrationTest, ContentletIndexAPIImplPhaseSwitchIntegrationTest) create indices directly via esImpl().createIndex() / osIndexAPI.createIndex()none drive ContentletDataGen inside a phase context. That is the missing capability.

Proposed approach

Add a phase override to ContentletDataGen, applied per-checkin around the checkin() call in persist(), restoring the prior flag value in a finally:

private MigrationPhase overridePhase = null;          // phase-oriented (primary)

public ContentletDataGen migrationPhase(final MigrationPhase phase) {
    this.overridePhase = phase;
    return this;
}

// store-oriented convenience, implemented on top of the phase override
public enum TargetStore { ES, OS, BOTH }
public ContentletDataGen targetStore(final TargetStore store) {
    this.overridePhase = switch (store) {
        case ES   -> MigrationPhase.PHASE_0_MIGRATION_NOT_STARTED; // ES only
        case OS   -> MigrationPhase.PHASE_3_OPENSEARCH_ONLY;       // OS only
        case BOTH -> MigrationPhase.PHASE_1_DUAL_WRITE_ES_READS;   // dual write
    };
    return this;
}

@Override
public Contentlet persist(final Contentlet contentlet) {
    final String prior = Config.getStringProperty(MigrationPhase.FLAG_KEY, null); // save prior
    try {
        if (overridePhase != null) {
            Config.setProperty(MigrationPhase.FLAG_KEY, String.valueOf(overridePhase.ordinal()));
        }
        return checkin(contentlet, null != policy ? policy : IndexPolicy.FORCE);
    } finally {
        if (overridePhase != null) {
            Config.setProperty(MigrationPhase.FLAG_KEY, prior); // restore prior, NOT null
        }
    }
}

Usage:

new ContentletDataGen(contentType)
    .targetStore(TargetStore.BOTH)   // or .migrationPhase(PHASE_1_DUAL_WRITE_ES_READS)
    .setPolicy(IndexPolicy.WAIT_FOR)
    .nextPersisted();                // → written to both ES and OS

Acceptance Criteria

  • ContentletDataGen exposes a phase-oriented builder method (migrationPhase(MigrationPhase)) that overrides the active migration phase only for the duration of the persist/checkin.
  • ContentletDataGen exposes a store-oriented convenience method (targetStore(ES | OS | BOTH)) implemented on top of the phase override (ES→Phase 0, OS→Phase 3, BOTH→dual-write Phase 1).
  • Override scope is per-checkin: the flag is set immediately before the checkin() call in persist() and restored afterward — sequential nextPersisted() calls on different gens do not leak phase state.
  • The override restores the previous flag value (captured before the call), never blindly clears it, so a test that set a phase globally is not clobbered.
  • The flag is restored even when checkin() throws (restore lives in a finally).
  • Backward compatible: with no override set, the gen respects the globally-active phase exactly as today — no behavior change for existing tests.
  • Override composes with setPolicy(...) (e.g. .migrationPhase(...).setPolicy(WAIT_FOR)), and WAIT_FOR still allows immediate assertion of index contents.
  • Publish/archive/delete/unpublish variants (nextPersistedAndPublish, etc.) honor the same phase override.
  • A new integration test lives in dotcms-integration, ends in the literal IT suffix, and is registered in OpenSearchUpgradeSuite. It asserts:
    • BOTH (dual-write) → contentlet present in both ES and OS indices.
    • OS (Phase 3) → present in OS, absent from ES.
    • ES (Phase 0) → present in ES, absent from OS.

Out of Scope

  • IndexAPI<F> generic parameterization (tracked separately, must not be bundled here).
  • Any change to production routing / PhaseRouter behavior — this is test-infrastructure only.

Additional Context

  • Architecture reference: docs/backend/OPENSEARCH_MIGRATION.md.
  • MigrationPhase enum + FLAG_KEY: dotCMS/src/main/java/com/dotcms/content/index/IndexConfigHelper.java.
  • All migration tests must be isolated in OpenSearchUpgradeSuite (project convention).

Priority: Medium


Progress

  • Branch: issue-36266-contentletdatagen-phase-control
  • ContentletDataGen capability implemented (migrationPhase(...) + targetStore(...), per-checkin scope, restore-prior + restore-on-throw, applied to both persist(...) overloads and nextPersistedAndPublish()) — compiles (BUILD SUCCESS).
  • ⏳ Store-placement IT in OpenSearchUpgradeSuite — pending; requires a live two-store (opensearch-upgrade) environment to author/verify and works around the known content-lifecycle bootstrap gap (inferIndexToHit on un-bootstrapped VersionedIndices).

Metadata

Metadata

Assignees

No one assigned

    Type

    No fields configured for Task.

    Projects

    Status
    New

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions