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
Expand Up @@ -5924,6 +5924,14 @@
"type": "string",
"description": "The source that declares the templated route the route was created from (only present when known)"
},
"checksum": {
"type": "string",
"description": "The checksum of the route as written, the same in every run: compare it with an earlier run to tell whether the route changed (only present when known, as it needs camel-xml-io)"
},
"revision": {
"type": "integer",
"description": "The revision of the route in this run: 1 when it is added, one more each time it is added again with another checksum, such as by a reload (only present when the checksum is known)"
},
"state": {
"type": "string",
"description": "The route state"
Expand Down Expand Up @@ -6678,6 +6686,10 @@
"type": "string",
"description": "The source location (only present when known)"
},
"checksum": {
"type": "string",
"description": "The checksum of the route as written, the same in every run: the dump is of this version of the route (only present when known, as it needs camel-xml-io)"
},
"format": {
"type": "string",
"description": "The dump format, xml\/yaml\/java (only present when the dump succeeded)"
Expand Down Expand Up @@ -6974,6 +6986,10 @@
"type": "string",
"description": "The route description (only present when configured)"
},
"checksum": {
"type": "string",
"description": "The checksum of the route as written, the same in every run: the facts a tool keeps per line hold for this version of the route (only present when known, as it needs camel-xml-io)"
},
"code": {
"type": "array",
"items": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,10 @@
"type": "string",
"description": "The source location (only present when known)"
},
"checksum": {
"type": "string",
"description": "The checksum of the route as written, the same in every run: the dump is of this version of the route (only present when known, as it needs camel-xml-io)"
},
"format": {
"type": "string",
"description": "The dump format, xml\/yaml\/java (only present when the dump succeeded)"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,10 @@
"type": "string",
"description": "The route description (only present when configured)"
},
"checksum": {
"type": "string",
"description": "The checksum of the route as written, the same in every run: the facts a tool keeps per line hold for this version of the route (only present when known, as it needs camel-xml-io)"
},
"code": {
"type": "array",
"items": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,14 @@
"type": "string",
"description": "The source that declares the templated route the route was created from (only present when known)"
},
"checksum": {
"type": "string",
"description": "The checksum of the route as written, the same in every run: compare it with an earlier run to tell whether the route changed (only present when known, as it needs camel-xml-io)"
},
"revision": {
"type": "integer",
"description": "The revision of the route in this run: 1 when it is added, one more each time it is added again with another checksum, such as by a reload (only present when the checksum is known)"
},
"state": {
"type": "string",
"description": "The route state"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.camel.component.kamelet;

import java.util.Map;
import java.util.TreeMap;

import org.apache.camel.CamelContext;
import org.apache.camel.Route;
import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.impl.DefaultCamelContext;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;

/**
* CAMEL-25771: a Kamelet route gets its node prefix, and its id when it has none of its own, from a counter, which the
* checksum leaves out: the same Kamelet route has the same checksum each time it is created.
*/
class KameletRouteChecksumTest {

@Test
void theSameKameletRouteHasTheSameChecksumWhenCreatedAgain() throws Exception {
// the counter goes on in the second run, as it does when a reload creates the routes again
Map<String, String> first = checksums();
Map<String, String> second = checksums();

assertEquals(2, first.size(), first.toString());
first.values().forEach(checksum -> assertNotNull(checksum, first.toString()));
assertEquals(first, second);
}

// the checksums of the Kamelet routes: the one with an id of its own, and the one without
private static Map<String, String> checksums() throws Exception {
try (CamelContext context = new DefaultCamelContext()) {
context.addRoutes(new RouteBuilder() {
@Override
public void configure() {
routeTemplate("greeting").templateParameter("name")
.from("kamelet:source")
.setBody().simple("Hello {{name}}");

from("direct:own").routeId("own").kamelet("greeting/greet-own?name=own");
from("direct:none").routeId("none").kamelet("greeting?name=none");
}
});
context.start();
Map<String, String> answer = new TreeMap<>();
for (Route route : context.getRoutes()) {
if ("greeting".equals(route.getRouteTemplateId())) {
answer.put("greet-own".equals(route.getId()) ? "own" : "none", route.getChecksum());
}
}
return answer;
}
}
}
15 changes: 15 additions & 0 deletions core/camel-api/src/main/java/org/apache/camel/NamedRoute.java
Original file line number Diff line number Diff line change
Expand Up @@ -100,4 +100,19 @@ default Resource getTemplatedRouteResource() {
return null;
}

/**
* Gets the checksum of the route as written: its steps, options and expressions, with property placeholders as
* written, without the ids Camel generates and where the route is in its source. The same route gives the same
* checksum in every run, so tools can tell which routes changed. It is computed when the route is prepared, before
* the parts of route configurations are added to it.
*
* @return the checksum, or null when it is not known (camel-xml-io is not on the classpath)
* @see org.apache.camel.spi.ModelChecksumGenerator
* @since 4.24
*/
@Nullable
default String getChecksum() {
return null;
}

}
28 changes: 28 additions & 0 deletions core/camel-api/src/main/java/org/apache/camel/Route.java
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,34 @@ default Resource getTemplatedRouteResource() {
return null;
}

/**
* Gets the checksum of the route as written: its steps, options and expressions, with property placeholders as
* written, without the ids Camel generates and where the route is in its source, and with the secrets in endpoint
* URIs masked. The same route gives the same checksum in every run, so tools can tell which routes changed between
* two runs.
*
* @return the checksum, or null when it is not known (camel-xml-io is not on the classpath)
* @see org.apache.camel.spi.ModelChecksumGenerator
* @since 4.24
*/
@Nullable
default String getChecksum() {
return null;
}

/**
* Gets the revision of the route in this run: 1 when the route is added, one more each time it is added again with
* another checksum, such as when a reload changed it. A reload that brings the route back as it was keeps the
* revision. The revision follows the route id, and starts over when the application restarts (tools compare
* checksums across runs). Camel keeps the revisions of the 10000 route ids used last.
*
* @return the revision, or 0 when it is not known (the checksum is not known)
* @since 4.24
*/
default int getRevision() {
return 0;
}

/**
* The source:line-number where the route input is located in the source code
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.camel.spi;

import org.apache.camel.CamelContext;
import org.apache.camel.NamedNode;
import org.jspecify.annotations.Nullable;

/**
* Computes the checksum of a route as written, so tools can tell which routes changed between two runs or after a
* reload: the same route gives the same checksum in every run.
* <p/>
* The checksum is a hash of the whole route: its steps, their options and expressions, with property placeholders as
* written. It leaves out what does not change the route: the ids Camel generates, and where the route is in its source
* (moving the route in its file, or adding a route above it, keeps its checksum).
* <p/>
* The implementation is provided by {@code camel-xml-io}, which writes every option of the model, and is discovered via
* the service key {@link #FACTORY}. It is a single implementation, so a route gets the same checksum whichever modules
* an application has.
*
* @since 4.24
*/
public interface ModelChecksumGenerator {

/**
* Service factory key.
*/
String FACTORY = "model-checksum-generator";

/**
* Computes the checksum of the definition as written.
*
* @param context the CamelContext
* @param definition the definition, such as a route
* @return the checksum, a short hex digest, or <tt>null</tt> when it cannot be computed
*/
@Nullable
String generateChecksum(CamelContext context, NamedNode definition);
}
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@
import org.apache.camel.spi.ManagementStrategy;
import org.apache.camel.spi.MessageHistoryFactory;
import org.apache.camel.spi.MessageSizeStrategy;
import org.apache.camel.spi.ModelChecksumGenerator;
import org.apache.camel.spi.ModelJAXBContextFactory;
import org.apache.camel.spi.ModelToJavaDumper;
import org.apache.camel.spi.ModelToStructureDumper;
Expand Down Expand Up @@ -228,6 +229,8 @@ public abstract class AbstractCamelContext extends BaseService
implements CatalogCamelContext, Suspendable {

private static final Logger LOG = LoggerFactory.getLogger(AbstractCamelContext.class);
// how many route ids keep their revision (CAMEL-25771)
private static final int MAX_ROUTE_REVISIONS = 10000;

private final InternalServiceManager internalServiceManager;

Expand All @@ -251,6 +254,15 @@ public abstract class AbstractCamelContext extends BaseService
private final StopWatch stopWatch = new StopWatch(false);
private final ThreadLocal<Set<String>> componentsInCreation = ThreadLocal.withInitial(HashSet::new);
private final Lock routesLock = new ReentrantLock();
// the revisions of the routes by id, guarded by routesLock (CAMEL-25771): the ids used least recently go first
// once there are more than MAX_ROUTE_REVISIONS, so an application that keeps adding routes with new ids, such as
// Kamelet routes without an id of their own, does not grow it for the life of the JVM
private final Map<String, RouteRevision> routeRevisions = new LinkedHashMap<>(16, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<String, RouteRevision> eldest) {
return size() > MAX_ROUTE_REVISIONS;
}
};
private final Lock lock = new ReentrantLock();
private VetoCamelContextStartException vetoed;
private String managementName;
Expand Down Expand Up @@ -411,6 +423,7 @@ protected void initPlugins() {
camelContextExtension.lazyAddContextPlugin(ModelToYAMLDumper.class, this::createModelToYAMLDumper);
camelContextExtension.lazyAddContextPlugin(ModelToJavaDumper.class, this::createModelToJavaDumper);
camelContextExtension.lazyAddContextPlugin(ModelToStructureDumper.class, this::createModelToStructureDumper);
camelContextExtension.lazyAddContextPlugin(ModelChecksumGenerator.class, this::createModelChecksumGenerator);
camelContextExtension.lazyAddContextPlugin(DeferServiceFactory.class, this::createDeferServiceFactory);
camelContextExtension.lazyAddContextPlugin(AnnotationBasedProcessorFactory.class,
this::createAnnotationBasedProcessorFactory);
Expand Down Expand Up @@ -4688,6 +4701,15 @@ protected void failOnStartup(Exception e) {

protected abstract ModelToStructureDumper createModelToStructureDumper();

/**
* The generator of the checksums of routes, or <tt>null</tt> without camel-xml-io on the classpath: then the routes
* have no checksum.
*/
protected ModelChecksumGenerator createModelChecksumGenerator() {
return ResolverHelper.resolveBootstrapService(getCamelContextReference(), ModelChecksumGenerator.FACTORY,
ModelChecksumGenerator.class).orElse(null);
}

protected abstract RestBindingJaxbDataFormatFactory createRestBindingJaxbDataFormatFactory();

protected abstract RestBindingJacksonXmlDataFormatFactory createRestBindingJacksonXmlDataFormatFactory();
Expand Down Expand Up @@ -4767,11 +4789,41 @@ public void addRoute(Route route) {
routesLock.lock();
try {
routes.add(route);
// RouteService has set it before the route was announced; this is for a route added another way
reviseRoute(route);
} finally {
routesLock.unlock();
}
}

/**
* Sets the revision of a route being added: 1 the first time its id is added, one more when it is added again with
* another checksum (a reload that changed it), the same when it comes back as it was. It is kept by route id, so it
* outlives the route being removed and added again; the {@value #MAX_ROUTE_REVISIONS} ids used last are kept. The
* same route revised again keeps its revision, so RouteService sets it before the route added event and
* {@link #addRoute(Route)} after it.
*/
void reviseRoute(Route route) {
String checksum = route.getChecksum();
if (route.getId() == null || checksum == null || !(route instanceof DefaultRoute defaultRoute)) {
return;
}
routesLock.lock();
try {
RouteRevision previous = routeRevisions.get(route.getId());
int revision = previous == null ? 1
: previous.checksum().equals(checksum) ? previous.revision() : previous.revision() + 1;
routeRevisions.put(route.getId(), new RouteRevision(checksum, revision));
defaultRoute.setRevision(revision);
} finally {
routesLock.unlock();
}
}

/** The checksum a route id was last added with, and its revision. */
private record RouteRevision(String checksum, int revision) {
}

public void removeRoute(Route route) {
routesLock.lock();
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ public class DefaultRoute extends ServiceSupport implements Route {
private final String sourceLocation;
private final String sourceLocationShort;
private final String routeTemplateId;
private final String checksum;
private volatile int revision;
private final Resource templatedRouteResource;
private final List<Processor> eventDrivenProcessors = new ArrayList<>();
private final List<InterceptStrategy> interceptStrategies = new ArrayList<>(0);
Expand Down Expand Up @@ -139,6 +141,7 @@ public DefaultRoute(CamelContext camelContext, NamedNode route, String routeId,
NamedRoute namedRoute = route instanceof NamedRoute nr ? nr : null;
this.routeTemplateId = namedRoute != null ? namedRoute.getRouteTemplateId() : null;
this.templatedRouteResource = namedRoute != null ? namedRoute.getTemplatedRouteResource() : null;
this.checksum = namedRoute != null ? namedRoute.getChecksum() : null;
}

@Override
Expand Down Expand Up @@ -233,6 +236,21 @@ public String getRouteTemplateId() {
return routeTemplateId;
}

@Override
public String getChecksum() {
return checksum;
}

@Override
public int getRevision() {
return revision;
}

/** Set by the CamelContext when the route is added: see {@link Route#getRevision()}. */
void setRevision(int revision) {
this.revision = revision;
}

@Override
public Resource getTemplatedRouteResource() {
return templatedRouteResource;
Expand Down
Loading
Loading