Skip to content

Make GeoIp downloader multi-project aware #128282

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 43 commits into
base: main
Choose a base branch
from

Conversation

samxbr
Copy link
Contributor

@samxbr samxbr commented May 22, 2025

This change makes the GeoIp persistent task executor/downloader multi-project aware.

  • the database downloader persistent task will be at the project level, meaning there will be a downloader instance per project
  • persistent task id is prefixed with project id, namely <project-id>/geoip-downloader for cluster in MP mode

To keep the size of PR review friendly, this PR only focus on the downloading part of GeoIP database, there will be more changes coming in separate PRs to make GeoIP multi-project aware in general.

@samxbr samxbr requested a review from Copilot June 8, 2025 09:38
Copilot

This comment was marked as outdated.

@samxbr samxbr requested a review from Copilot June 8, 2025 11:25
@samxbr samxbr marked this pull request as ready for review June 8, 2025 11:26
@elasticsearchmachine elasticsearchmachine added the Team:Data Management Meta label for data/management team label Jun 8, 2025
@elasticsearchmachine
Copy link
Collaborator

Pinging @elastic/es-data-management (Team:Data Management)

Copy link
Contributor

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR refactors the GeoIP downloader to be aware of multiple projects by scoping persistent tasks, routing table lookups, and state updates per project.

  • Introduces project-scoped task IDs and a ProjectResolver to manage downloaders per project.
  • Extends ClusterChangedEvent, task executor, downloader, transport actions, and tests to pass and respect ProjectId.
  • Adds a multi-project integration test and updates existing unit tests for project context.

Reviewed Changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
server/src/test/java/org/elasticsearch/cluster/ClusterChangedEventTests.java Added testChangedCustomProjectMetadataSet to verify per-project custom metadata changes.
server/src/main/java/org/elasticsearch/cluster/ClusterChangedEvent.java Added changedCustomProjectMetadataSet(ProjectId) for per-project metadata diffs.
muted-tests.yml Unmuted old ingest-geoip client YAML tests.
modules/ingest-geoip/src/test/java/org/elasticsearch/ingest/geoip/GeoIpDownloaderTests.java Updated unit tests to initialize and pass ProjectId and ProjectResolver.
modules/ingest-geoip/src/test/java/org/elasticsearch/ingest/geoip/GeoIpDownloaderTaskExecutorTests.java Replaced cluster-state APIs in tests with project-scoped builders.
modules/ingest-geoip/src/test/java/org/elasticsearch/ingest/geoip/DatabaseNodeServiceTests.java Extended createClusterState helpers to accept ProjectId.
modules/ingest-geoip/src/main/java/org/elasticsearch/ingest/geoip/GeoIpStatsTransportAction.java Injected ProjectResolver and fetch task per project.
modules/ingest-geoip/src/main/java/org/elasticsearch/ingest/geoip/IngestGeoIpPlugin.java Wired ProjectResolver into the downloader executor.
modules/ingest-geoip/src/main/java/org/elasticsearch/ingest/geoip/GeoIpDownloaderTaskExecutor.java Refactored to maintain a map of downloaders per project, use per-project settings, and handle cluster changes.
modules/ingest-geoip/src/main/java/org/elasticsearch/ingest/geoip/GeoIpDownloader.java Made downloader project-aware in index lookups, logging, and boundary checks.
modules/ingest-geoip/qa/multi-project/src/javaRestTest/java/geoip/GeoIpMultiProjectIT.java New REST integration test for multi-project GeoIP download behavior.
modules/ingest-geoip/qa/multi-project/build.gradle Added the multi-project QA module to build and test.
Comments suppressed due to low confidence (1)

modules/ingest-geoip/src/main/java/org/elasticsearch/ingest/geoip/GeoIpDownloaderTaskExecutor.java:239

  • There are no unit tests covering the multi‐project clusterChanged logic for starting and stopping tasks per project. Add tests to verify behavior when settings change, pipelines are added/removed, and projects are deleted.
if (event.metadataChanged() == false) {

GeoIpDownloader currentDownloader = getCurrentTask();
if (currentDownloader != null) {
currentDownloader.requestReschedule();
for (var projectMetadataEntry : event.state().metadata().projects().entrySet()) {
Copy link
Preview

Copilot AI Jun 8, 2025

Choose a reason for hiding this comment

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

The clusterChanged method only iterates over newly present projects, so removed projects never trigger task removal. Consider also iterating previousState.metadata().projects() to stop tasks for projects that were deleted.

Copilot uses AI. Check for mistakes.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not sure if this is valid. To delete a project, the related persistent tasks ought to be cancelled before the project metadata is deleted from the cluster state, otherwise all sorts of things can go wrong. Project deletion should probably be handled separately.

Copy link
Contributor

Choose a reason for hiding this comment

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

the related persistent tasks ought to be cancelled before the project metadata is deleted from the cluster state, otherwise all sorts of things can go wrong

Yep, I agree. Project deletion is currently still being discussed, so we can leave this for now. Can you put a @FixForMultiProject with a comment to remind us that we'll want to have some safeguards in place for project deletion? I'm sure we'll want to have safeguards, but I'm not sure yet what we should do as a safeguard, hence the annotation reminder.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Should the safeguard be at the place where project deletion is handled (probably non-existing yet) instead instead of here? Perhaps it's better to track this safeguarding in the work for project deletion in general. I added a comment here as a reminder.

Copy link
Contributor

Choose a reason for hiding this comment

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

I'd prefer that we put the annotation here as well. That way we're sure it won't be forgotten.

Copy link
Contributor

@nielsbauman nielsbauman left a comment

Choose a reason for hiding this comment

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

Thanks for working on this, Sam! And thanks for splitting the changes up into a very comfortable size :)

  • Can you also update the title and/or final squashed commit message to be a bit more specific?
  • Did you try running the GeoIP YAML tests in MP mode too? Or do those require some more work?

GeoIpDownloader currentDownloader = getCurrentTask();
if (currentDownloader != null) {
currentDownloader.requestReschedule();
for (var projectMetadataEntry : event.state().metadata().projects().entrySet()) {
Copy link
Contributor

Choose a reason for hiding this comment

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

the related persistent tasks ought to be cancelled before the project metadata is deleted from the cluster state, otherwise all sorts of things can go wrong

Yep, I agree. Project deletion is currently still being discussed, so we can leave this for now. Can you put a @FixForMultiProject with a comment to remind us that we'll want to have some safeguards in place for project deletion? I'm sure we'll want to have safeguards, but I'm not sure yet what we should do as a safeguard, hence the annotation reminder.

@samxbr samxbr changed the title Make GeoIp multi-project aware Make GeoIp downloader multi-project aware Jun 10, 2025
@samxbr
Copy link
Contributor Author

samxbr commented Jun 10, 2025

  • Can you also update the title and/or final squashed commit message to be a bit more specific?

Updated the title, and will for sure squash the commits before merging

  • Did you try running the GeoIP YAML tests in MP mode too? Or do those require some more work?

The GeoIP yaml tests requires more work in MP, which is loading the databases from system index to ingest node. To limit the size of this PR, I will include that part in another PR

@samxbr samxbr requested a review from nielsbauman June 17, 2025 16:10
if (clusterService.state().nodes().isLocalNodeElectedMaster() == false) {
// we should only start/stop task from single node, master is the best as it will go through it anyway
return;
}
if (enabled) {
startTask(() -> {});
startTask(projectResolver.getProjectId(), () -> {});
Copy link
Contributor

Choose a reason for hiding this comment

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

This only works because the project resolver currently still returns a fallback project ID (which will be removed in the future). This will need to be updated in the future anyway as well (when these settings are made project-aware), but I'd still prefer that we just hard-code ProjectId.DEFAULT instead. That way we at least don't rely on temporary behavior. The same goes for the methods/settings below.

GeoIpDownloader currentDownloader = getCurrentTask();
if (currentDownloader != null) {
currentDownloader.requestReschedule();
for (var projectMetadataEntry : event.state().metadata().projects().entrySet()) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd prefer that we put the annotation here as well. That way we're sure it won't be forgotten.

Comment on lines +229 to +231
for (var projectMetadataEntry : event.state().metadata().projects().entrySet()) {
ProjectId projectId = projectMetadataEntry.getKey();
ProjectMetadata projectMetadata = projectMetadataEntry.getValue();
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
for (var projectMetadataEntry : event.state().metadata().projects().entrySet()) {
ProjectId projectId = projectMetadataEntry.getKey();
ProjectMetadata projectMetadata = projectMetadataEntry.getValue();
for (var projectMetadata : event.state().metadata().projects().values()) {
ProjectId projectId = projectMetadata.id();

Comment on lines +234 to +237
boolean taskIsBootstrapped = taskIsBootstrappedByProject.computeIfAbsent(projectId, k -> false);
if (taskIsBootstrapped != true) {
taskIsBootstrappedByProject.put(projectId, true);
this.taskIsBootstrappedByProject.computeIfAbsent(projectId, k -> hasAtLeastOneGeoipProcessor(projectMetadata));
Copy link
Contributor

Choose a reason for hiding this comment

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

Ah, I missed that taskIsBootstrappedByProject is used async in startTask and stopTask. The current code isn't 100% airtight. I was checking on ConcurrentHashMap if there's anything we can use, but I don't think so. ConcurrentHashMap#replace is close, but it doesn't account for unset/null values... So, I think we have to go back to the AtomicBoolean inside the map again, sorry about that. I think the other maps are OK like this.

Comment on lines +259 to +262
var atLeastOneGeoipProcessor = atLeastOneGeoipProcessorByProject.computeIfAbsent(projectId, k -> false);
boolean newAtLeastOneGeoipProcessor = hasAtLeastOneGeoipProcessor(projectMetadata);

if (newAtLeastOneGeoipProcessor && atLeastOneGeoipProcessor == false) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
var atLeastOneGeoipProcessor = atLeastOneGeoipProcessorByProject.computeIfAbsent(projectId, k -> false);
boolean newAtLeastOneGeoipProcessor = hasAtLeastOneGeoipProcessor(projectMetadata);
if (newAtLeastOneGeoipProcessor && atLeastOneGeoipProcessor == false) {
var atLeastOneGeoipProcessor = atLeastOneGeoipProcessorByProject.getOrDefault(projectId, false);
if (atLeastOneGeoipProcessor == false && hasAtLeastOneGeoipProcessor(projectMetadata)) {

and move the put below to inside the if-block.

  • doing getOrDefault instead of computIfAbsent avoids the redundant new value
  • performing the (relatively expensive check) as the second condition allows us to skip it if we already set this value to true once before (as it'll never get set to false, we only remove the entry)
  • we don't need to put false on every run

Comment on lines +433 to 434
.projectState(projectId)
.metadata()
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
.projectState(projectId)
.metadata()
.metadata()
.getProject(projectId)

No need to convert to ProjectState here.

@@ -287,4 +281,14 @@ private ClusterState clusterStateWithIndex(Consumer<Settings.Builder> consumer,
.build();
return ClusterState.builder(ClusterName.DEFAULT).putProjectMetadata(project).build();
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we can remove the clusterStateWithIndex method, right?

Comment on lines +172 to +182
Set<String> changed = new HashSet<>();
ProjectMetadata project = state.metadata().projects().get(projectId);
ProjectMetadata previousProject = previousState.metadata().projects().get(projectId);
if (previousProject != null && project != null) {
changed.addAll(changedCustoms(project.customs(), previousProject.customs()));
} else if (previousProject != null) {
changed.addAll(previousProject.customs().keySet());
} else if (project != null) {
changed.addAll(project.customs().keySet());
}
return changed.contains(customMetadataType);
Copy link
Contributor

Choose a reason for hiding this comment

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

This still computes the entire Set unnecessarily. What do you think about something like this:

Suggested change
Set<String> changed = new HashSet<>();
ProjectMetadata project = state.metadata().projects().get(projectId);
ProjectMetadata previousProject = previousState.metadata().projects().get(projectId);
if (previousProject != null && project != null) {
changed.addAll(changedCustoms(project.customs(), previousProject.customs()));
} else if (previousProject != null) {
changed.addAll(previousProject.customs().keySet());
} else if (project != null) {
changed.addAll(project.customs().keySet());
}
return changed.contains(customMetadataType);
ProjectMetadata previousProject = previousState.metadata().projects().get(projectId);
ProjectMetadata project = state.metadata().projects().get(projectId);
Object previousValue = previousProject == null ? null : previousProject.customs().get(customMetadataType);
Object value = project == null ? null : project.customs().get(customMetadataType);
return Objects.equals(previousValue, value) == false;

.putProjectMetadata(ProjectMetadata.builder(project2).build())
.build();
ClusterChangedEvent event = new ClusterChangedEvent("_na_", newState, originalState);
assertTrue(event.customMetadataChanged(project2.id(), IndexGraveyard.TYPE));
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this IndexGraveyard line a leftover, or did you put it there intentionally?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
:Data Management/Ingest Node Execution or management of Ingest Pipelines including GeoIP >non-issue Team:Data Management Meta label for data/management team v9.1.0
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants