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 @@ -385,10 +385,53 @@ protected void onRouteReload(Collection<Resource> resources, boolean removeEvery
}
}
} catch (Exception e) {
// the routes that ran before were removed above and the new ones failed to load: the app has no routes
// until the next successful reload. Restore the previous routes now, without the failed resources, so a
// mistake in one file leaves the rest running (CAMEL-24860); the failed file loads on its next save
restorePreviousRoutes(resources, e);
throw RuntimeCamelException.wrapRuntimeException(e);
}
}

/**
* Reloads the sources of the routes that ran before a failed reload, without the resources that failed, so a
* mistake in one file does not leave the application without routes. After a successful restore the remembered set
* is cleared: the running routes are the last working set again, and the next reload collects their sources itself.
* The failed file is loaded again on its next save.
*/
protected void restorePreviousRoutes(Collection<Resource> failed, Exception cause) {
Comment thread
davsclaus marked this conversation as resolved.
if (!removeAllRoutes || previousSources.isEmpty()) {
return;
}
List<Resource> restore = new ArrayList<>();
for (Resource rs : previousSources) {
if (rs != null && (failed == null || !equalResourceLocation(failed, rs))) {
restore.add(rs);
}
}
if (restore.isEmpty()) {
LOG.warn("Reload failed and there are no previous routes to restore: the application runs without routes"
+ " until the file is fixed");
return;
}
try {
// a partial load may have left routes or endpoints behind
getCamelContext().getRouteController().removeAllRoutes();
getCamelContext().removeRouteTemplates("*");
getCamelContext().getEndpointRegistry().clear();
Set<String> ids = PluginHelper.getRoutesLoader(getCamelContext()).updateRoutes(restore);
// the running routes are the last working set again: the next reload collects their sources itself
previousSources.clear();
LOG.warn("Reload failed due to: {}. The previous routes were restored ({} route(s) running); the changed"
+ " file loads on its next save",
cause.getMessage(), ids.size());
} catch (Exception e) {
LOG.warn("Reload failed and the previous routes could not be restored due to: {}. The application runs"
+ " without routes until the file is fixed",
e.getMessage(), e);
}
}

/**
* Whether the target is loading any of the given sources
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -473,6 +473,14 @@ set here, because that would reject input that parses today. Routes that genuine
an external DTD or parameter entity through this converter must supply their own
`SAXParserFactory`.

=== camel-core - a failed route reload restores the previous routes

When a route file is reloaded in dev mode (`camel run --dev`, or the route watcher reload strategy in general) and
the new content fails to load, the routes that ran before are restored right away, without the routes of the failed
file, and a WARN says so. Before, the previous routes stayed stopped until the next successful reload, so a mistake
in one file left the application without routes. The failed file loads again on its next save. The
`CamelContextReloadFailure` event and the reload error log line are unchanged.

=== camel-core - the type of a bean created by a script or a builder is optional

The `type` (class name) of a bean definition — `bean` under `beans`, `templateBean` of a route
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/*
* 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.dsl.yaml

import org.apache.camel.ServiceStatus
import org.apache.camel.dsl.yaml.support.YamlTestSupport
import org.apache.camel.spi.Resource
import org.apache.camel.support.ResourceHelper
import org.apache.camel.support.RouteWatcherReloadStrategy

import java.nio.file.Files
import java.nio.file.Path

/**
* CAMEL-24860: a reload that fails (a route file saved with a mistake) restores the routes that ran before, instead
* of leaving the application without routes until the next successful save.
*/
class RouteReloadRollbackTest extends YamlTestSupport {

Path dir
Path good
Path bad

@Override
def doSetup() {
dir = Files.createTempDirectory("camel-reload")
good = dir.resolve("good.camel.yaml")
bad = dir.resolve("bad.camel.yaml")
Files.writeString(good, '''
- route:
id: good
from:
uri: direct:good
steps:
- to:
uri: mock:good
''')
Files.writeString(bad, '''
- route:
id: bad
from:
uri: direct:bad
steps:
- to:
uri: mock:bad
''')
context.start()
loadRoutes(ResourceHelper.resolveResource(context, "file:" + good), ResourceHelper.resolveResource(context, "file:" + bad))
}

def cleanup() {
dir.toFile().deleteDir()
}

def 'a failed reload restores the previous routes'() {
setup:
def strategy = new RouteWatcherReloadStrategy(dir.toString())
strategy.setCamelContext(context)
strategy.setPattern("*.yaml")
// the strategy is not started (no file watcher in the test): its reload callback is driven by hand
strategy.doStart()
assert context.getRouteController().getRouteStatus("good") == ServiceStatus.Started
assert context.getRouteController().getRouteStatus("bad") == ServiceStatus.Started
when: 'the second file is saved with a mistake'
Files.writeString(bad, '''
- route:
id: bad
from:
uri: direct:bad
steps:
- pollEnrich:
uri: file:./order.json
''')
def failure = null
try {
strategy.getResourceReload().onReload(bad.toString(), ResourceHelper.resolveResource(context, "file:" + bad))
} catch (Exception e) {
failure = e
}
then: 'the reload fails, and the route of the other file runs again'
failure != null
context.getRouteController().getRouteStatus("good") == ServiceStatus.Started
context.getRoute("bad") == null
when: 'the file is fixed'
Files.writeString(bad, '''
- route:
id: bad
from:
uri: direct:bad
steps:
- to:
uri: mock:bad
''')
strategy.getResourceReload().onReload(bad.toString(), ResourceHelper.resolveResource(context, "file:" + bad))
then: 'both run'
context.getRouteController().getRouteStatus("good") == ServiceStatus.Started
context.getRouteController().getRouteStatus("bad") == ServiceStatus.Started
cleanup:
strategy.doStop()
}
}
Loading