Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions .github/workflows/java-playwright-external.yml
Original file line number Diff line number Diff line change
Expand Up @@ -396,8 +396,10 @@ jobs:

scale-it-external:
# Runs after search-it (shared instance, singleton reindex app). max-parallel: 1 so the matrix
# entries (e.g. 100k then 500k) seed + reindex one at a time — two concurrent reindexes on one
# cluster collide and the db/es counts would mix cohorts.
# entries seed + reindex one at a time — two concurrent reindexes on one cluster collide and the
# db/es counts would mix cohorts. Default cohort is 100k only; a 500k run took >5h and was
# cancelled every night without ever completing. Larger sizes can still be requested on demand
# via the scaleSeeds dispatch input (e.g. [100000, 500000]).
needs: [search-it-external]
if: ${{ always() && github.event.inputs.runScaleIt != 'false' }}
runs-on: ubuntu-latest
Expand All @@ -406,7 +408,7 @@ jobs:
fail-fast: false
max-parallel: 1
matrix:
seed: ${{ fromJSON(github.event.inputs.scaleSeeds || '[100000, 500000]') }}
seed: ${{ fromJSON(github.event.inputs.scaleSeeds || '[100000]') }}
steps:
- name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@main
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,20 +121,27 @@ void searchFieldsSelectWhichFieldMatches(final TestNamespace ns) {

final SearchSettings base = currentSettings();

// Field selection is proven by which seeded table is IN vs OUT: name-only must match the
// name-carrier and exclude the description-carrier (and vice versa). Use
// contains/doesNotContain
// rather than containsExactly — on the shared cluster a foreign ngram-colliding table can add a
// hit without changing which of OUR two tables the searched field selects.
SearchSettings nameOnly = SearchSettingsTestHelper.copyOf(base);
SearchSettingsTestHelper.setOnlySearchField(nameOnly, TABLE_INDEX, NAME_FIELD, 5.0);
nameOnly = SearchSettingsTestHelper.withRankingDisabled(nameOnly, TABLE_INDEX);
assertThat(SearchSettingsTestHelper.previewIds(server, query, TABLE_INDEX, nameOnly, 10))
.as("searching only 'name' must return only the table whose name carries the token")
.containsExactly(tokenInName.getId().toString());
.as("searching only 'name' must match the name-carrier, not the description-carrier")
.contains(tokenInName.getId().toString())
.doesNotContain(tokenInDescription.getId().toString());

SearchSettings descriptionOnly = SearchSettingsTestHelper.copyOf(base);
SearchSettingsTestHelper.setOnlySearchField(
descriptionOnly, TABLE_INDEX, DESCRIPTION_FIELD, 5.0);
descriptionOnly = SearchSettingsTestHelper.withRankingDisabled(descriptionOnly, TABLE_INDEX);
assertThat(SearchSettingsTestHelper.previewIds(server, query, TABLE_INDEX, descriptionOnly, 10))
.as("searching only 'description' must return only the table whose description carries it")
.containsExactly(tokenInDescription.getId().toString());
.as("searching only 'description' must match the description-carrier, not the name-carrier")
.contains(tokenInDescription.getId().toString())
.doesNotContain(tokenInName.getId().toString());
}

@Test
Expand Down Expand Up @@ -193,7 +200,8 @@ void matchTypeExactRequiresTheFullKeyword(final TestNamespace ns) {
final DatabaseSchema schema = DatabaseSchemaTestFactory.createSimple(ns);
final Table exactTable =
RelevancyFixtures.createTable(schema, exactName, "plaindescription", null);
RelevancyFixtures.createTable(schema, exactName + "extra", "plaindescription", null);
final Table prefixedTable =
RelevancyFixtures.createTable(schema, exactName + "extra", "plaindescription", null);
awaitIndexed(exactName, 2);

final SearchSettings base = currentSettings();
Expand All @@ -210,9 +218,13 @@ void matchTypeExactRequiresTheFullKeyword(final TestNamespace ns) {
SearchSettingsTestHelper.setOnlySearchField(
standard, TABLE_INDEX, NAME_FIELD, 5.0, FieldBoost.MatchType.STANDARD);
standard = SearchSettingsTestHelper.withRankingDisabled(standard, TABLE_INDEX);
// STANDARD is a fuzzy name.ngram match, so on the shared cluster a foreign ngram-colliding
// table can add hits beyond the two we seeded. Assert both seeded ids are present rather than
// an exact size (mirrors maxResultHitsClampsTheReturnedHits), which such a foreign hit
// inflates.
assertThat(SearchSettingsTestHelper.previewIds(server, exactName, TABLE_INDEX, standard, 10))
.as("matchType=standard must match both the exact and the prefixed name")
.hasSize(2);
.contains(exactTable.getId().toString(), prefixedTable.getId().toString());
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,6 @@
public final class ExternalTokenRefresher implements AutoCloseable {

private static final Logger LOG = LoggerFactory.getLogger(ExternalTokenRefresher.class);
// Renew this far ahead of expiry so a flaky login endpoint has room for several 60s retries
// before the token actually dies (login TTL is ~1h, so 10m early is safe).
private static final Duration EXPIRY_BUFFER = Duration.ofMinutes(10);
// Bounds BOTH the connect and the read phase of each login. Without a read timeout a slow or
// half-open gateway would block http.send() forever on the single-thread scheduler, and the
// token would never renew for the rest of the run.
Expand Down Expand Up @@ -125,12 +122,18 @@ private String login() {
}
}

// Renew at half the token's remaining lifetime (~30m on the external cluster's ~1h token) rather
// than a fixed buffer before expiry. That leaves ~half the TTL as retry runway if the login
// endpoint is unreachable at renewal time: a ~10m login-endpoint outage that lands on the renewal
// window (observed 2026-07-17) burned through a fixed 10m buffer and let the token expire
// mid-seed; half-life gives the 60s retries far more room to catch the endpoint before the token
// actually dies.
private long refreshDelaySeconds(final String token) {
long delaySeconds = FALLBACK_REFRESH_SECONDS;
final Long exp = expiryEpochSeconds(token);
if (exp != null) {
final long now = System.currentTimeMillis() / 1000;
delaySeconds = Math.max(60, exp - now - EXPIRY_BUFFER.toSeconds());
delaySeconds = Math.max(60, (exp - now) / 2);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Aged Tokens Refresh Later

When the configured token is already partway through its lifetime, this calculation can reduce the retry runway. A token with 10 minutes remaining previously refreshed after the 60-second floor, leaving about 9 minutes for retries; it now waits 5 minutes, so a five-minute login outage can let the token expire and cause external-cluster requests to return 401.

Context Used: CLAUDE.md (source)

}
return delaySeconds;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -187,8 +187,17 @@ public DataQualityListPage clickPrevious() {

/** Open the page-size dropdown and assert the standard 3 options render. */
public DataQualityListPage assertPageSizeOptionsCount(final int expected) {
openMenu(byTestId("page-size-selection-dropdown"), page.locator(".ant-dropdown-menu"));
PlaywrightAssertions.assertThat(page.locator(".ant-dropdown-menu-item")).hasCount(expected);
// Scope to the OPEN overlay: every Ant <Dropdown> on the page (nav bar, help, etc.) portals a
// .ant-dropdown-menu to <body> and keeps it in the DOM while hidden, so a bare
// .ant-dropdown-menu first-matches a foreign hidden menu and the visibility wait times out.
// :not(.ant-dropdown-hidden) pins it to the just-opened page-size menu (mirrors the
// .ant-select-dropdown idiom above).
final Locator pageSizeMenu =
page.locator(".ant-dropdown:not(.ant-dropdown-hidden) .ant-dropdown-menu");
openMenu(byTestId("page-size-selection-dropdown"), pageSizeMenu);
PlaywrightAssertions.assertThat(
page.locator(".ant-dropdown:not(.ant-dropdown-hidden) .ant-dropdown-menu-item"))
.hasCount(expected);
return this;
}

Expand Down
Loading