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
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* Copyright (c) 2010-2026 Eclipse Dirigible contributors
*
* All rights reserved. This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v2.0 which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v20.html
*
* SPDX-FileCopyrightText: Eclipse Dirigible contributors SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.dirigible.components.base.registry;

import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;

import org.springframework.stereotype.Component;

/**
* Tracks client requests that mutate the registry, so the synchronization reconciler can tell a
* half-applied write from a finished one.
*
* <p>
* A publish replaces a collection by DELETING it and copying it back milliseconds later, and both
* steps can happen inside a single request. A reconciler that looks into that hole sees sources
* that are about to reappear as deleted. It therefore asks here whether a mutation was in flight,
* rather than inferring it from "the registry changed" - the file-system watcher reports a genuine
* deletion exactly the same way, and treating the two alike would delay every deletion by a pass.
*/
@Component
public class RegistryMutationTracker {

/** Registry-mutating requests currently being served. */
private final AtomicInteger inFlight = new AtomicInteger();

/** Registry-mutating requests served so far - lets an observer detect one that came and went. */
private final AtomicLong completed = new AtomicLong();

/** Marks the start of a registry-mutating request. */
public void enter() {
inFlight.incrementAndGet();
}

/** Marks the end of a registry-mutating request. */
public void exit() {
inFlight.decrementAndGet();
completed.incrementAndGet();
}

/**
* Whether a registry-mutating request is being served right now.
*
* @return true if at least one such request is in flight
*/
public boolean isMutating() {
return inFlight.get() > 0;
}

/**
* The number of registry-mutating requests served so far. Compare a value taken earlier with the
* current one to find out whether a mutation completed in between.
*
* @return the count of completed registry-mutating requests
*/
public long completedMutations() {
return completed.get();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import org.eclipse.dirigible.components.base.artefact.topology.TopologyWrapper;
import org.eclipse.dirigible.components.base.healthcheck.status.HealthCheckStatus;
import org.eclipse.dirigible.components.base.healthcheck.status.HealthCheckStatus.Jobs.JobStatus;
import org.eclipse.dirigible.components.base.registry.RegistryMutationTracker;
import org.eclipse.dirigible.components.base.synchronizer.SynchronizationWatcher;
import org.eclipse.dirigible.components.base.synchronizer.Synchronizer;
import org.eclipse.dirigible.components.base.synchronizer.SynchronizerCallback;
Expand Down Expand Up @@ -78,6 +79,9 @@ public class SynchronizationProcessor implements SynchronizationWalkerCallback,
/** The synchronization watcher. */
private final SynchronizationWatcher synchronizationWatcher;

/** Tells whether a client is publishing to (or unpublishing from) the registry. */
private final RegistryMutationTracker registryMutationTracker;

/** The initialized. */
private final AtomicBoolean initialized;

Expand All @@ -94,16 +98,18 @@ public class SynchronizationProcessor implements SynchronizationWalkerCallback,
* @param synchronizers the synchronizers
* @param definitionService the definition service
* @param synchronizationWatcher the synchronization watcher
* @param registryMutationTracker the registry mutation tracker
*/
@Autowired
public SynchronizationProcessor(IRepository repository, List<Synchronizer<?, ?>> synchronizers, DefinitionService definitionService,
SynchronizationWatcher synchronizationWatcher) {
SynchronizationWatcher synchronizationWatcher, RegistryMutationTracker registryMutationTracker) {
this.repository = repository;
this.synchronizers = Collections.synchronizedList(synchronizers);
logger.info("Registered [{}] synchronizers: [{}]", synchronizers.size(), synchronizers);

this.definitionService = definitionService;
this.synchronizationWatcher = synchronizationWatcher;
this.registryMutationTracker = registryMutationTracker;
this.synchronizers.forEach(s -> s.setCallback(this));

this.initialized = new AtomicBoolean(false);
Expand Down Expand Up @@ -200,6 +206,9 @@ public void processSynchronizers() {
.passStarted();
processing.set(true);
synchronizationWatcher.reset();
// Sampled before the walk, compared at cleanup: a publish that came and went while this pass
// ran is as dangerous as one still in flight.
long mutationsBeforePass = registryMutationTracker.completedMutations();

try {

Expand Down Expand Up @@ -389,15 +398,35 @@ public void processSynchronizers() {

logger.trace("Cleaning up removed artefacts...");

// cleanup
// An artefact whose source is gone is removed - UNLESS a publish was in flight while this
// pass ran. A publish replaces a collection by DELETING it and copying it back
// milliseconds later, and a pass that looks into that hole deletes artefacts whose
// sources are about to reappear. That is not cosmetic: a reconciler that rebuilds from
// the WHOLE artefact set - the client-Java batch compile - then compiles a half-empty
// codebase, the batch fails as a whole, and the instance is left with no controllers
// registered until something else changes.
//
// The question asked is "was a client writing to the registry", NOT "did the registry
// change": the file-system watcher reports a genuine deletion exactly like a publish, so
// keying on it would defer every deletion by a pass. A publish always ends by marking the
// registry modified, which schedules the next pass - and forceProcessSynchronizers()
// waits for it - so the deferred cleanup happens promptly.
boolean registryChangedDuringPass =
registryMutationTracker.isMutating() || registryMutationTracker.completedMutations() != mutationsBeforePass;
for (Synchronizer synchronizer : synchronizers) {
List<? extends Artefact> registered = synchronizer.getService()
.getAll();
for (Artefact artefact : registered) {
if (synchronizer.isAccepted(artefact.getType())) {
if (!repository.getResource(IRepositoryStructure.PATH_REGISTRY_PUBLIC + artefact.getLocation())
.exists()) {
synchronizer.cleanup(artefact);
if (registryChangedDuringPass) {
logger.info(
"Source of artefact [{}] is missing, but the registry changed during this pass - deferring its cleanup to the next one",
artefact.getLocation());
} else {
synchronizer.cleanup(artefact);
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -222,9 +222,17 @@ private void rebuildAll() {
List<JavaLoader.ClientSource> sources = new ArrayList<>(all.size());
for (JavaFile file : all) {
if (!RegistrySourceLoader.exists(file.getLocation())) {
// The artefact's source disappeared between cycles; let the orchestrator's cleanup
// loop delete the artefact row. We must not include a stale source here.
continue;
// The source of a still-registered artefact is momentarily gone - a publish replaces
// a collection by deleting it and copying it back. Compiling now would submit a
// knowingly incomplete batch: every class referencing a missing one fails, javac can
// emit no bytecode for the whole batch, and the client codebase falls back to its
// last-good state - or, on a first publish, to nothing at all. Rebuild on the next
// cycle instead, by when the file is back or the orchestrator's cleanup has dropped
// the artefact.
LOGGER.info("Java source [{}] is registered but currently missing - deferring the rebuild to the next cycle",
file.getLocation());
dirty.set(true);
return;
}
byte[] bytes;
try {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* Copyright (c) 2010-2026 Eclipse Dirigible contributors
*
* All rights reserved. This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v2.0 which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v20.html
*
* SPDX-FileCopyrightText: Eclipse Dirigible contributors SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.dirigible.components.ide.workspace.filter;

import java.io.IOException;

import org.eclipse.dirigible.components.base.registry.RegistryMutationTracker;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

/**
* Reports a publish / unpublish request to the {@link RegistryMutationTracker} for as long as it is
* being served, so the synchronization reconciler does not mistake the gap between the delete and
* the copy of a replaced collection for a deletion.
*
* <p>
* Only mutating methods count: the IDE polls these endpoints with GET all the time, and counting
* those would keep the registry permanently "in mutation".
*/
@Component
class RegistryMutationFilter extends OncePerRequestFilter {

private final RegistryMutationTracker tracker;

RegistryMutationFilter(RegistryMutationTracker tracker) {
this.tracker = tracker;
}

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
tracker.enter();
try {
chain.doFilter(request, response);
} finally {
tracker.exit();
}
}

/**
* Should not filter.
*
* @param request the request
* @return true for requests that cannot mutate the registry
*/
@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
return HttpMethod.GET.matches(request.getMethod()) || HttpMethod.HEAD.matches(request.getMethod());
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright (c) 2010-2026 Eclipse Dirigible contributors
*
* All rights reserved. This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v2.0 which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v20.html
*
* SPDX-FileCopyrightText: Eclipse Dirigible contributors SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.dirigible.components.ide.workspace.filter;

import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
* Registers the {@link RegistryMutationFilter} on the endpoints that publish to - or unpublish from
* - the registry. Registering it explicitly also keeps Spring Boot from mapping it to every
* request: a write through a generated application's REST controller is not a registry mutation.
*/
@Configuration
class RegistryMutationFilterConfig {

/** The publisher and workspace endpoints - the client paths that write to the registry. */
private static final String[] URL_PATTERNS = { //
"/services/ide/publisher/*", //
"/services/ide/workspace/*", //
"/services/ide/workspaces/*"};

@Bean
FilterRegistrationBean<RegistryMutationFilter> registryMutationFilterRegistrationBean(RegistryMutationFilter filter) {
FilterRegistrationBean<RegistryMutationFilter> registration = new FilterRegistrationBean<>(filter);
registration.addUrlPatterns(URL_PATTERNS);
return registration;
}

}
Loading
Loading