This is an automated email from the ASF dual-hosted git repository.
heneveld pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/brooklyn-server.git
The following commit(s) were added to refs/heads/master by this push:
new 8116df80e1 workflow errors are simplified when serialized;
8116df80e1 is described below
commit 8116df80e13dabeaf250e789946f70d62970e9ef
Author: Alex Heneveld <[email protected]>
AuthorDate: Mon Aug 7 21:24:29 2023 +0100
workflow errors are simplified when serialized;
cuts down on size, and more importantly prevents issues where errors
contian non-serializable context
---
.../core/workflow/WorkflowErrorHandling.java | 5 +-
.../core/workflow/WorkflowExecutionContext.java | 7 +-
.../workflow/WorkflowExpressionResolution.java | 2 +-
.../WorkflowStepInstanceExecutionContext.java | 44 +++++--
.../util/core/xstream/SafeThrowableConverter.java | 117 +++++++++++++++++
.../brooklyn/util/core/xstream/XmlSerializer.java | 106 ++++++++++++---
.../mgmt/persist/XmlMementoSerializerTest.java | 75 ++++++++---
.../workflow/WorkflowPersistReplayErrorsTest.java | 6 +-
.../core/workflow/WorkflowPersistSpecialTest.java | 116 +++++++++++++++++
.../util/core/xstream/XmlSerializerTest.java | 36 ++++-
.../workflow/workflow-with-error-with-trace.xml | 145 +++++++++++++++++++++
.../util/exceptions/LossySerializingThrowable.java | 50 +++++++
12 files changed, 655 insertions(+), 54 deletions(-)
diff --git
a/core/src/main/java/org/apache/brooklyn/core/workflow/WorkflowErrorHandling.java
b/core/src/main/java/org/apache/brooklyn/core/workflow/WorkflowErrorHandling.java
index e12bc0062f..ea39a0d1ff 100644
---
a/core/src/main/java/org/apache/brooklyn/core/workflow/WorkflowErrorHandling.java
+++
b/core/src/main/java/org/apache/brooklyn/core/workflow/WorkflowErrorHandling.java
@@ -44,7 +44,8 @@ public class WorkflowErrorHandling implements
Callable<WorkflowErrorHandling.Wor
private static final Logger log =
LoggerFactory.getLogger(WorkflowErrorHandling.class);
/*
- * TODO ui for error handling
+ * ui for error handling
+ *
* step task's workflow tag will have ERROR_HANDLED_BY single-key map tag
pointing at handler parent task, created in this method.
* error handler parent task will have 'errorHandlerForTask' field in the
workflow tag pointing at step task, but no errorHandlerIndex.
* if any of the handler steps match, the parent will have
ERROR_HANDLED_BY pointing at it, and will have a task with the workflow tag with
@@ -135,7 +136,7 @@ public class WorkflowErrorHandling implements
Callable<WorkflowErrorHandling.Wor
WorkflowStepInstanceExecutionContext handlerContext = new
WorkflowStepInstanceExecutionContext(stepIndexIfStepErrorHandler!=null ?
stepIndexIfStepErrorHandler :
WorkflowExecutionContext.STEP_INDEX_FOR_ERROR_HANDLER, errorStep, context);
context.errorHandlerContext = handlerContext;
- handlerContext.error = error;
+ handlerContext.setError(error);
lastStepConditionMatched = false;
String potentialTaskName =
Tasks.current().getDisplayName()+"-"+(i+1);
diff --git
a/core/src/main/java/org/apache/brooklyn/core/workflow/WorkflowExecutionContext.java
b/core/src/main/java/org/apache/brooklyn/core/workflow/WorkflowExecutionContext.java
index f97666c1bc..750d835500 100644
---
a/core/src/main/java/org/apache/brooklyn/core/workflow/WorkflowExecutionContext.java
+++
b/core/src/main/java/org/apache/brooklyn/core/workflow/WorkflowExecutionContext.java
@@ -861,6 +861,9 @@ public class WorkflowExecutionContext {
return false;
}
+ // for json
+ private void setMostRecentActivityTime(Object ignored) {}
+
/** look in tasks, steps, and replays to find most recent activity */
// keep on jackson serialization for api?
public long getMostRecentActivityTime() {
@@ -1534,7 +1537,7 @@ public class WorkflowExecutionContext {
try {
handleErrorAtStep(step, t, onFinish, e);
} catch (Exception e2) {
- currentStepInstance.error = e2;
+ currentStepInstance.setError(e2);
throw e2;
}
} finally {
@@ -1544,7 +1547,7 @@ public class WorkflowExecutionContext {
log.warn("Lost old step info for " + this + ", step "
+ index);
old = new OldStepRecord();
}
- if (currentStepInstance.error==null) old.countCompleted++;
+ if (currentStepInstance.getError()==null)
old.countCompleted++;
// okay if this gets picked up by accident because we will
check the stepIndex it records against the currentStepIndex,
// and ignore it if different
old.context = currentStepInstance;
diff --git
a/core/src/main/java/org/apache/brooklyn/core/workflow/WorkflowExpressionResolution.java
b/core/src/main/java/org/apache/brooklyn/core/workflow/WorkflowExpressionResolution.java
index 20e0b11a4c..dd1603880b 100644
---
a/core/src/main/java/org/apache/brooklyn/core/workflow/WorkflowExpressionResolution.java
+++
b/core/src/main/java/org/apache/brooklyn/core/workflow/WorkflowExpressionResolution.java
@@ -275,7 +275,7 @@ public class WorkflowExpressionResolution {
//error (if there is an error in scope)
WorkflowStepInstanceExecutionContext currentStepInstance =
context.currentStepInstance;
WorkflowStepInstanceExecutionContext errorHandlerContext =
context.errorHandlerContext;
- if ("error".equals(key)) return
TemplateProcessor.wrapAsTemplateModel(errorHandlerContext!=null ?
errorHandlerContext.error : null);
+ if ("error".equals(key)) return
TemplateProcessor.wrapAsTemplateModel(errorHandlerContext!=null ?
errorHandlerContext.getError() : null);
if ("input".equals(key)) return
TemplateProcessor.wrapAsTemplateModel(context.input);
if ("output".equals(key)) return
TemplateProcessor.wrapAsTemplateModel(context.getOutput());
diff --git
a/core/src/main/java/org/apache/brooklyn/core/workflow/WorkflowStepInstanceExecutionContext.java
b/core/src/main/java/org/apache/brooklyn/core/workflow/WorkflowStepInstanceExecutionContext.java
index ee2c1379d7..b6c6dee997 100644
---
a/core/src/main/java/org/apache/brooklyn/core/workflow/WorkflowStepInstanceExecutionContext.java
+++
b/core/src/main/java/org/apache/brooklyn/core/workflow/WorkflowStepInstanceExecutionContext.java
@@ -23,6 +23,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonSetter;
import com.google.common.reflect.TypeToken;
+import com.thoughtworks.xstream.annotations.XStreamAlias;
import org.apache.brooklyn.api.entity.Entity;
import org.apache.brooklyn.api.mgmt.ManagementContext;
import org.apache.brooklyn.config.ConfigKey;
@@ -31,6 +32,7 @@ import org.apache.brooklyn.core.mgmt.BrooklynTaskTags;
import org.apache.brooklyn.util.collections.MutableMap;
import org.apache.brooklyn.util.collections.MutableSet;
import org.apache.brooklyn.util.exceptions.Exceptions;
+import org.apache.brooklyn.util.exceptions.LossySerializingThrowable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -71,11 +73,28 @@ public class WorkflowStepInstanceExecutionContext {
// replay instructions or a string explicit next step identifier
public Object next;
- /** set if the step is in an error handler context, containing the error
being handled,
- * or if the step had an error that was not handled */
- Throwable error;
+ /** Return any error we are handling, if the step is in an error handler,
+ * or an unhandled error if the step is not in an error handler,
+ * otherwise null.
+ *
+ * After persistence, this stores a simplified form of the error (via
{@link LossySerializingThrowable}). */
+ public Throwable getError() {
+ if (error==null && errorRecord!=null) error = errorRecord.getError();
+ return error;
+ }
+ transient Throwable error;
+ @XStreamAlias("error")
+ @JsonIgnore
+ Throwable errorLegacyDeserialized;
+ @JsonIgnore
+ LossySerializingThrowable errorRecord;
+ void setError(Throwable t) {
+ error = t;
+ errorRecord = new LossySerializingThrowable(error);
+ }
+ /** The Jackson error is always just a string. */
@JsonGetter("error") String getErrorForJson() { return
Exceptions.collapseText(error); }
- @JsonSetter("error") void setErrorFromJaon(String error) { this.error =
new RuntimeException(error); }
+ @JsonSetter("error") void setErrorFromJson(String error) { setError(new
RuntimeException(error)); }
/** set if there was an error handled locally */
String errorHandlerTaskId;
@@ -192,11 +211,6 @@ public class WorkflowStepInstanceExecutionContext {
return subWorkflows;
}
- /** Return any error we are handling, if we are an error handler;
otherwise null */
- public Throwable getError() {
- return error;
- }
-
public TypeToken<?> lookupType(String type, Supplier<TypeToken<?>>
ifUnset) {
return context.lookupType(type, ifUnset);
}
@@ -221,7 +235,7 @@ public class WorkflowStepInstanceExecutionContext {
@JsonIgnore
public String getWorkflowStepReference() {
- return context==null ?
"unknown-"+stepDefinitionDeclaredId+"-"+stepIndex :
context.getWorkflowStepReference(stepIndex, stepDefinitionDeclaredId,
error!=null);
+ return context==null ?
"unknown-"+stepDefinitionDeclaredId+"-"+stepIndex :
context.getWorkflowStepReference(stepIndex, stepDefinitionDeclaredId,
getError()!=null);
}
@JsonIgnore
@@ -242,4 +256,14 @@ public class WorkflowStepInstanceExecutionContext {
public String toString() {
return
"WorkflowStepInstanceExecutionContext{"+getWorkflowStepReference()+" /
"+getName()+"}";
}
+
+ // standard deserialization method
+ private WorkflowStepInstanceExecutionContext readResolve() {
+ if (errorLegacyDeserialized!=null && errorRecord==null) {
+ setError(errorLegacyDeserialized);
+ errorLegacyDeserialized = null;
+ }
+ return this;
+ }
+
}
diff --git
a/core/src/main/java/org/apache/brooklyn/util/core/xstream/SafeThrowableConverter.java
b/core/src/main/java/org/apache/brooklyn/util/core/xstream/SafeThrowableConverter.java
new file mode 100644
index 0000000000..fc3c84cad8
--- /dev/null
+++
b/core/src/main/java/org/apache/brooklyn/util/core/xstream/SafeThrowableConverter.java
@@ -0,0 +1,117 @@
+/*
+ * 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.brooklyn.util.core.xstream;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.thoughtworks.xstream.converters.Converter;
+import com.thoughtworks.xstream.converters.MarshallingContext;
+import com.thoughtworks.xstream.converters.UnmarshallingContext;
+import com.thoughtworks.xstream.core.DefaultConverterLookup;
+import com.thoughtworks.xstream.core.util.QuickWriter;
+import com.thoughtworks.xstream.io.HierarchicalStreamReader;
+import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
+import com.thoughtworks.xstream.io.path.PathTrackingWriter;
+import
org.apache.brooklyn.util.core.xstream.XmlSerializer.PrettyPrintWriterExposingStack;
+import org.apache.brooklyn.util.exceptions.Exceptions;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.net.Inet4Address;
+import java.net.UnknownHostException;
+import java.util.function.Predicate;
+
+/** This is a hacky way to try to recover in some non-serializable situations.
But it can and often does
+ * generate XML which cannot be read back, because it will omit fields which
might be required to create an object.
+ */
+public class SafeThrowableConverter implements Converter {
+
+ private static final Logger log =
LoggerFactory.getLogger(SafeThrowableConverter.class);
+ private final DefaultConverterLookup converterLookup;
+ private final Predicate<Class> supportedTypes;
+
+ ThreadLocal<Object> converting = new ThreadLocal<>();
+
+ @VisibleForTesting
+ public static int TODO = 0;
+
+ public SafeThrowableConverter(Predicate<Class> supportedTypes,
DefaultConverterLookup converterLookup) {
+ this.supportedTypes = supportedTypes;
+ this.converterLookup = converterLookup;
+ }
+
+ @Override
+ public boolean canConvert(@SuppressWarnings("rawtypes") Class type) {
+ return converting.get()==null && supportedTypes.test(type);
+ }
+
+ @Override
+ public void marshal(Object source, HierarchicalStreamWriter writer,
MarshallingContext context) {
+ boolean wrappedHere = false;
+
+ int depth = -1;
+ HierarchicalStreamWriter w2 = writer;
+ try {
+ if (converting.get()!=null) {
+ // shouldn't come here; above should prevent it
+ wrappedHere = false;
+ } else {
+ wrappedHere = true;
+ converting.set(source);
+ // flush cache so it recomputes whether we can convert (we
cannot anymore)
+ converterLookup.flushCache();
+ while (w2 instanceof PathTrackingWriter) { w2 =
w2.underlyingWriter(); }
+ if (w2 instanceof PrettyPrintWriterExposingStack) {
+ depth = ((PrettyPrintWriterExposingStack)w2).path.depth();
+ }
+ }
+ try {
+ context.convertAnother(source);
+ } catch (Exception e) {
+ Exceptions.propagateIfFatal(e);
+ if (depth<0) throw Exceptions.propagate(e);
+
+ log.debug("Unable to convert "+source+"; will abandon keys
that aren't valid: "+e);
+ while
(((PrettyPrintWriterExposingStack)w2).path.depth()>depth) {
+ writer.endNode();
+ writer.flush();
+ try {
+ ((PrettyPrintWriterExposingStack)
w2).getOrigWriter().write("<!-- fields omitted in previous node due to error
-->");
+ } catch (IOException ex) {
+ throw Exceptions.propagate(ex);
+ }
+ }
+ }
+ } finally {
+ if (wrappedHere) {
+ converting.set(null);
+ converterLookup.flushCache();
+ }
+ }
+ }
+
+ @Override
+ public Object unmarshal(HierarchicalStreamReader reader,
UnmarshallingContext context) {
+ converting.set("not for use with unmarshalling");
+ // flush cache so it recomputes whether we can convert (we cannot
anymore)
+ converterLookup.flushCache();
+ return context.convertAnother(null, context.getRequiredType());
+ }
+
+}
diff --git
a/core/src/main/java/org/apache/brooklyn/util/core/xstream/XmlSerializer.java
b/core/src/main/java/org/apache/brooklyn/util/core/xstream/XmlSerializer.java
index ace24c78a6..2484a2c6a3 100644
---
a/core/src/main/java/org/apache/brooklyn/util/core/xstream/XmlSerializer.java
+++
b/core/src/main/java/org/apache/brooklyn/util/core/xstream/XmlSerializer.java
@@ -19,42 +19,52 @@
package org.apache.brooklyn.util.core.xstream;
import com.google.common.annotations.VisibleForTesting;
-import com.google.common.collect.Iterables;
-import com.google.common.collect.Maps;
-import java.io.Reader;
-import java.io.StringReader;
-import java.io.StringWriter;
-import java.io.Writer;
-import java.util.LinkedHashMap;
-import java.util.LinkedHashSet;
-import java.util.Map;
-import java.util.Set;
-
-import java.util.function.Function;
-import org.apache.brooklyn.util.collections.MutableList;
-import org.apache.brooklyn.util.collections.MutableMap;
-import org.apache.brooklyn.util.collections.MutableSet;
-
import com.google.common.base.Supplier;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Maps;
import com.thoughtworks.xstream.XStream;
+import com.thoughtworks.xstream.XStreamException;
import com.thoughtworks.xstream.converters.extended.JavaClassConverter;
+import com.thoughtworks.xstream.core.ClassLoaderReference;
+import com.thoughtworks.xstream.core.DefaultConverterLookup;
+import com.thoughtworks.xstream.core.util.CompositeClassLoader;
+import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
+import com.thoughtworks.xstream.io.naming.NameCoder;
+import com.thoughtworks.xstream.io.path.PathTracker;
+import com.thoughtworks.xstream.io.xml.PrettyPrintWriter;
+import com.thoughtworks.xstream.io.xml.XppDriver;
import com.thoughtworks.xstream.mapper.DefaultMapper;
import com.thoughtworks.xstream.mapper.Mapper;
import com.thoughtworks.xstream.mapper.MapperWrapper;
+import org.apache.brooklyn.util.collections.MutableList;
+import org.apache.brooklyn.util.collections.MutableMap;
+import org.apache.brooklyn.util.collections.MutableSet;
import org.apache.brooklyn.util.exceptions.Exceptions;
import org.apache.brooklyn.util.guava.Maybe;
import org.apache.brooklyn.util.javalang.Reflections;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import java.io.Reader;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Function;
+
public class XmlSerializer<T> {
private static final Logger LOG =
LoggerFactory.getLogger(XmlSerializer.class);
private final Map<String, String> deserializingClassRenames;
protected final XStream xstream;
+ protected final XppDriver hierarchicalStreamDriver;
+ protected final DefaultConverterLookup converterLookup;
public XmlSerializer() {
this(null);
@@ -70,7 +80,19 @@ public class XmlSerializer<T> {
public XmlSerializer(ClassLoader loader, Map<String, String>
deserializingClassRenames, Function<MapperWrapper,MapperWrapper>
mapperCustomizer) {
this.deserializingClassRenames = deserializingClassRenames == null ?
ImmutableMap.of() : deserializingClassRenames;
- xstream = new XStream() {
+
+ hierarchicalStreamDriver = new XppDriver() {
+ public HierarchicalStreamWriter createWriter(Writer out) {
+ return new PrettyPrintWriterExposingStack(out, getNameCoder());
+ }
+ };
+
+ converterLookup = new DefaultConverterLookup();
+
+ xstream = new XStream(null, hierarchicalStreamDriver, new
ClassLoaderReference(new CompositeClassLoader()), (Mapper)null,
+ type -> converterLookup.lookupConverterForType(type),
+ (converter,priority) ->
converterLookup.registerConverter(converter, priority)
+ ) {
@Override
protected MapperWrapper wrapMapper(MapperWrapper next) {
MapperWrapper result =
XmlSerializer.this.wrapMapperForNormalUsage(super.wrapMapper(next));
@@ -87,10 +109,35 @@ public class XmlSerializer<T> {
xstream.setClassLoader(loader);
}
+// // we could accept losing fields in exceptions; usually they are
context that we don't care about; but it can generate XML which cannot be read
back
+// xstream.registerConverter(new SafeThrowableConverter(t ->
Throwable.class.isAssignableFrom(t), converterLookup));
+
xstream.registerConverter(newCustomJavaClassConverter(),
XStream.PRIORITY_NORMAL);
+
addStandardHelpers(xstream);
}
+ static class PrettyPrintWriterExposingStack extends PrettyPrintWriter {
+ private final Writer origWriter;
+
+ public PrettyPrintWriterExposingStack(Writer writer, NameCoder
nameCoder) { super(writer, nameCoder); this.origWriter = writer; }
+ public PathTracker path = new PathTracker();
+ public void startNode(String name) {
+ path.pushElement(name);
+ super.startNode(name);
+ }
+
+ @Override
+ public void endNode() {
+ super.endNode();
+ path.popElement();
+ }
+
+ public Writer getOrigWriter() {
+ return origWriter;
+ }
+ }
+
@VisibleForTesting
public static void addStandardHelpers(XStream xstream) {
@@ -228,8 +275,29 @@ public class XmlSerializer<T> {
return wrapMapperForHandlingClasses(next);
}
- public void serialize(Object object, Writer writer) {
- xstream.toXML(object, writer);
+ public void serialize(Object obj, Writer out) {
+// xstream.toXML(obj, writer);
+
+ // we replace the above (parent impl) with the following, expanded to
give better output for errors
+ // (mainly used for lambdas which are not serializable)
+ HierarchicalStreamWriter writer =
hierarchicalStreamDriver.createWriter(out);
+ try {
+ xstream.marshal(obj, writer);
+ } catch (Throwable e) {
+ Exceptions.propagateIfInterrupt(e);
+
+ if (writer instanceof PrettyPrintWriterExposingStack) {
+ String path = ("" +
((PrettyPrintWriterExposingStack)writer).path.getPath()).trim();
+ if (!e.toString().contains(path)) {
+ throw new XStreamException(Exceptions.collapseText(e) + ";
while converting element at " + path, e);
+ }
+ }
+
+ throw Exceptions.propagate(e);
+ } finally {
+ writer.flush();
+ }
+
}
@SuppressWarnings("unchecked")
diff --git
a/core/src/test/java/org/apache/brooklyn/core/mgmt/persist/XmlMementoSerializerTest.java
b/core/src/test/java/org/apache/brooklyn/core/mgmt/persist/XmlMementoSerializerTest.java
index 54d13b8dab..a81a4421e4 100644
---
a/core/src/test/java/org/apache/brooklyn/core/mgmt/persist/XmlMementoSerializerTest.java
+++
b/core/src/test/java/org/apache/brooklyn/core/mgmt/persist/XmlMementoSerializerTest.java
@@ -306,8 +306,8 @@ public class XmlMementoSerializerTest {
}
}
- private LookupContextImpl
newEmptyLookupManagementContext(ManagementContext managementContext, boolean
failOnDangling) {
- return new LookupContextImpl("empty context for test",
managementContext,
+ private LookupContextTestImpl
newEmptyLookupManagementContext(ManagementContext managementContext, boolean
failOnDangling) {
+ return new LookupContextTestImpl("empty context for test",
managementContext,
ImmutableList.<Entity>of(), ImmutableList.<Location>of(),
ImmutableList.<Policy>of(),
ImmutableList.<Enricher>of(), ImmutableList.<Feed>of(),
ImmutableList.<CatalogItem<?, ?>>of(), ImmutableList.<ManagedBundle>of(),
failOnDangling);
}
@@ -670,8 +670,8 @@ public class XmlMementoSerializerTest {
LOG.info("serializedForm=" + serializedForm);
return (T) serializer.fromString(serializedForm);
}
-
- static class LookupContextImpl implements LookupContext {
+
+ public static class LookupContextTestImpl implements LookupContext {
private final Stack<String> description;
private final ManagementContext mgmt;
private final Map<String, Entity> entities;
@@ -682,11 +682,9 @@ public class XmlMementoSerializerTest {
private final Map<String, CatalogItem<?, ?>> catalogItems;
private final Map<String, ManagedBundle> bundles;
private final boolean failOnDangling;
+ private boolean lookupInMgmtContext;
- LookupContextImpl(String description, ManagementContext mgmt,
Iterable<? extends Entity> entities, Iterable<? extends Location> locations,
- Iterable<? extends Policy> policies, Iterable<? extends
Enricher> enrichers, Iterable<? extends Feed> feeds,
- Iterable<? extends CatalogItem<?, ?>> catalogItems, Iterable<?
extends ManagedBundle> bundles,
- boolean failOnDangling) {
+ public LookupContextTestImpl(String description, ManagementContext
mgmt, boolean failOnDangling) {
this.description = new Stack<>();
this.description.push(description);
this.mgmt = mgmt;
@@ -697,6 +695,16 @@ public class XmlMementoSerializerTest {
this.feeds = Maps.newLinkedHashMap();
this.catalogItems = Maps.newLinkedHashMap();
this.bundles = Maps.newLinkedHashMap();
+ this.failOnDangling = failOnDangling;
+ this.lookupInMgmtContext = true;
+ }
+
+ public LookupContextTestImpl(String description, ManagementContext
mgmt, Iterable<? extends Entity> entities, Iterable<? extends Location>
locations,
+ Iterable<? extends Policy> policies,
Iterable<? extends Enricher> enrichers, Iterable<? extends Feed> feeds,
+ Iterable<? extends CatalogItem<?, ?>>
catalogItems, Iterable<? extends ManagedBundle> bundles,
+ boolean failOnDangling) {
+ this(description, mgmt, failOnDangling);
+ this.lookupInMgmtContext = false;
for (Entity entity : entities) this.entities.put(entity.getId(),
entity);
for (Location location : locations)
this.locations.put(location.getId(), location);
for (Policy policy : policies) this.policies.put(policy.getId(),
policy);
@@ -704,12 +712,12 @@ public class XmlMementoSerializerTest {
for (Feed feed : feeds) this.feeds.put(feed.getId(), feed);
for (CatalogItem<?, ?> catalogItem : catalogItems)
this.catalogItems.put(catalogItem.getId(), catalogItem);
for (ManagedBundle bundle : bundles)
this.bundles.put(bundle.getId(), bundle);
- this.failOnDangling = failOnDangling;
}
- LookupContextImpl(String description, ManagementContext mgmt,
Map<String,? extends Entity> entities, Map<String,? extends Location> locations,
- Map<String,? extends Policy> policies, Map<String,? extends
Enricher> enrichers, Map<String,? extends Feed> feeds,
- Map<String, ? extends CatalogItem<?, ?>> catalogItems,
Map<String,? extends ManagedBundle> bundles,
- boolean failOnDangling) {
+
+ public LookupContextTestImpl(String description, ManagementContext
mgmt, Map<String,? extends Entity> entities, Map<String,? extends Location>
locations,
+ Map<String,? extends Policy> policies,
Map<String,? extends Enricher> enrichers, Map<String,? extends Feed> feeds,
+ Map<String, ? extends CatalogItem<?, ?>>
catalogItems, Map<String,? extends ManagedBundle> bundles,
+ boolean failOnDangling) {
this.description = new Stack<>();
this.description.push(description);
this.mgmt = mgmt;
@@ -735,6 +743,10 @@ public class XmlMementoSerializerTest {
return mgmt;
}
@Override public Entity lookupEntity(String id) {
+ if (lookupInMgmtContext) {
+ Entity result = mgmt.lookup(id, Entity.class);
+ if (result != null) return result;
+ }
if (entities.containsKey(id)) {
return entities.get(id);
}
@@ -744,6 +756,10 @@ public class XmlMementoSerializerTest {
return null;
}
@Override public Location lookupLocation(String id) {
+ if (lookupInMgmtContext) {
+ Location result = mgmt.lookup(id, Location.class);
+ if (result != null) return result;
+ }
if (locations.containsKey(id)) {
return locations.get(id);
}
@@ -753,6 +769,10 @@ public class XmlMementoSerializerTest {
return null;
}
@Override public Policy lookupPolicy(String id) {
+ if (lookupInMgmtContext) {
+ Policy result = mgmt.lookup(id, Policy.class);
+ if (result != null) return result;
+ }
if (policies.containsKey(id)) {
return policies.get(id);
}
@@ -762,6 +782,10 @@ public class XmlMementoSerializerTest {
return null;
}
@Override public Enricher lookupEnricher(String id) {
+ if (lookupInMgmtContext) {
+ Enricher result = mgmt.lookup(id, Enricher.class);
+ if (result != null) return result;
+ }
if (enrichers.containsKey(id)) {
return enrichers.get(id);
}
@@ -771,6 +795,10 @@ public class XmlMementoSerializerTest {
return null;
}
@Override public Feed lookupFeed(String id) {
+ if (lookupInMgmtContext) {
+ Feed result = mgmt.lookup(id, Feed.class);
+ if (result != null) return result;
+ }
if (feeds.containsKey(id)) {
return feeds.get(id);
}
@@ -781,6 +809,10 @@ public class XmlMementoSerializerTest {
}
@Override public EntityAdjunct lookupAnyEntityAdjunct(String id) {
+ if (lookupInMgmtContext) {
+ EntityAdjunct result = mgmt.lookup(id, EntityAdjunct.class);
+ if (result != null) return result;
+ }
if (policies.containsKey(id)) {
return policies.get(id);
}
@@ -797,6 +829,10 @@ public class XmlMementoSerializerTest {
}
@Override public CatalogItem<?, ?> lookupCatalogItem(String id) {
+ if (lookupInMgmtContext) {
+ CatalogItem result = mgmt.lookup(id, CatalogItem.class);
+ if (result != null) return result;
+ }
if (catalogItems.containsKey(id)) {
return catalogItems.get(id);
}
@@ -806,6 +842,10 @@ public class XmlMementoSerializerTest {
return null;
}
@Override public ManagedBundle lookupBundle(String id) {
+ if (lookupInMgmtContext) {
+ ManagedBundle result = mgmt.lookup(id, ManagedBundle.class);
+ if (result != null) return result;
+ }
if (bundles.containsKey(id)) {
return bundles.get(id);
}
@@ -818,7 +858,12 @@ public class XmlMementoSerializerTest {
@Override
public BrooklynObject lookup(BrooklynObjectType type, String id) {
if (type==null) {
- BrooklynObject result = peek(null, id);
+ BrooklynObject result = null;
+ if (lookupInMgmtContext) {
+ result = mgmt.lookup(id, BrooklynObject.class);
+ if (result != null) return result;
+ }
+ result = peek(null, id);
if (result==null) {
if (failOnDangling) {
throw new NoSuchElementException("no brooklyn object
with id "+id+"; type not specified");
@@ -871,7 +916,7 @@ public class XmlMementoSerializerTest {
@SuppressWarnings("unchecked")
@VisibleForTesting
- public LookupContextImpl add(BrooklynObject object) {
+ public LookupContextTestImpl add(BrooklynObject object) {
if (object!=null) {
((Map<String,BrooklynObject>)
getMapFor(BrooklynObjectType.of(object))).put(object.getId(), object);
}
diff --git
a/core/src/test/java/org/apache/brooklyn/core/workflow/WorkflowPersistReplayErrorsTest.java
b/core/src/test/java/org/apache/brooklyn/core/workflow/WorkflowPersistReplayErrorsTest.java
index bb94e587f5..32481c4e77 100644
---
a/core/src/test/java/org/apache/brooklyn/core/workflow/WorkflowPersistReplayErrorsTest.java
+++
b/core/src/test/java/org/apache/brooklyn/core/workflow/WorkflowPersistReplayErrorsTest.java
@@ -1082,7 +1082,7 @@ public class WorkflowPersistReplayErrorsTest extends
RebindTestFixture<BasicAppl
WorkflowExecutionContext.OldStepRecord step2 = run.oldStepInfo.get(1);
Asserts.assertNotNull(step2);
Asserts.assertNotNull(step2.context);
- Asserts.assertNull(step2.context.error); // should be null because
handled
+ Asserts.assertNull(step2.context.getError()); // should be null
because handled
Asserts.assertNull(step2.context.errorHandlerTaskId); // should be
null because not treated as a step handler, but handler for the workflow -
step2sub.errorHandlerTaskId
BrooklynTaskTags.WorkflowTaskTag step2subTag =
Iterables.getOnlyElement(step2.context.getSubWorkflows());
@@ -1096,7 +1096,7 @@ public class WorkflowPersistReplayErrorsTest extends
RebindTestFixture<BasicAppl
Asserts.assertNotNull(step22);
Asserts.assertNotNull(step22.context);
- Asserts.assertNotNull(step22.context.error); // not null because not
handled here
+ Asserts.assertNotNull(step22.context.getError()); // not null
because not handled here
Asserts.assertNotNull(step22.context.errorHandlerTaskId);
}
@@ -1128,7 +1128,7 @@ public class WorkflowPersistReplayErrorsTest extends
RebindTestFixture<BasicAppl
WorkflowExecutionContext.OldStepRecord step22 =
step2sub.oldStepInfo.get(1);
Asserts.assertNotNull(step22);
Asserts.assertNotNull(step22.context);
- Asserts.assertNotNull(step22.context.error);
+ Asserts.assertNotNull(step22.context.getError());
Asserts.assertNotNull(step22.context.errorHandlerTaskId);
}
}
diff --git
a/core/src/test/java/org/apache/brooklyn/core/workflow/WorkflowPersistSpecialTest.java
b/core/src/test/java/org/apache/brooklyn/core/workflow/WorkflowPersistSpecialTest.java
new file mode 100644
index 0000000000..b8e78ea30f
--- /dev/null
+++
b/core/src/test/java/org/apache/brooklyn/core/workflow/WorkflowPersistSpecialTest.java
@@ -0,0 +1,116 @@
+/*
+ * 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.brooklyn.core.workflow;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.brooklyn.api.entity.EntitySpec;
+import org.apache.brooklyn.core.mgmt.internal.LocalManagementContext;
+import org.apache.brooklyn.core.mgmt.persist.XmlMementoSerializer;
+import org.apache.brooklyn.core.mgmt.persist.XmlMementoSerializerTest;
+import org.apache.brooklyn.core.mgmt.rebind.RebindOptions;
+import org.apache.brooklyn.core.mgmt.rebind.RebindTestFixture;
+import org.apache.brooklyn.core.resolve.jackson.BeanWithTypeUtils;
+import org.apache.brooklyn.entity.stock.BasicApplication;
+import org.apache.brooklyn.test.Asserts;
+import org.apache.brooklyn.util.core.ResourceUtils;
+import org.apache.brooklyn.util.exceptions.PropagatedRuntimeException;
+import org.apache.brooklyn.util.text.Strings;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.annotations.Test;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.io.StringWriter;
+
+public class WorkflowPersistSpecialTest extends
RebindTestFixture<BasicApplication> {
+
+ private static final Logger log =
LoggerFactory.getLogger(WorkflowPersistSpecialTest.class);
+
+ @Override
+ protected BasicApplication createApp() {
+ return
mgmt().getEntityManager().createEntity(EntitySpec.create(BasicApplication.class));
+ }
+
+ @Override
+ protected LocalManagementContext
decorateOrigOrNewManagementContext(LocalManagementContext mgmt) {
+ WorkflowBasicTest.addWorkflowStepTypes(mgmt);
+ return super.decorateOrigOrNewManagementContext(mgmt);
+ }
+
+ @Override protected BasicApplication rebind() throws Exception {
+ return
rebind(RebindOptions.create().terminateOrigManagementContext(true));
+ }
+
+ @Test
+ public void testSerializeWorkflowWithError() throws IOException {
+ BasicApplication app = createApp();
+ XmlMementoSerializer<Object> xml =
XmlMementoSerializer.XmlMementoSerializerBuilder.from(mgmt()).build();
+ xml.setLookupContext(new
XmlMementoSerializerTest.LookupContextTestImpl("testing", mgmt(), true));
+ ObjectMapper json = BeanWithTypeUtils.newMapper(mgmt(), false, null,
true);
+ WorkflowExecutionContext wc;
+ StringWriter sw;
+ String s;
+
+ // read legacy
+ s =
ResourceUtils.create(this).getResourceAsString(getClass().getPackage().getName().replaceAll("\\.",
"/")+"/workflow-with-error-with-trace.xml");
+ s = Strings.replaceAll(s, "__ENTITY_ID__", app.getId());
+ Asserts.assertStringContains(s, "<error ");
+ Asserts.assertStringContains(s,
"<trace>org.apache.brooklyn.core.workflow.WorkflowExecutionContext$Body.call");
+ wc = (WorkflowExecutionContext) xml.deserialize(new StringReader(s));
+ Asserts.assertInstanceOf(wc.getCurrentStepInstance().getError(),
PropagatedRuntimeException.class);
+
Asserts.assertNotNull(wc.getCurrentStepInstance().getError().getCause());
+ Asserts.assertNotNull(wc.getEntity());
+
+ // write above legacy, assert new format
+ sw = new StringWriter();
+ xml.serialize(wc, sw);
+ Asserts.assertStringDoesNotContain(sw.toString(), "<error ");
+ Asserts.assertStringDoesNotContain(sw.toString(),
"<trace>org.apache.brooklyn.core.workflow.WorkflowExecutionContext$Body.call");
+
+ // and json can write and read
+ s = json.writeValueAsString(wc);
+ Asserts.assertStringContains(s, "\"error\":\"WorkflowFailException: ");
+ Asserts.assertStringDoesNotContain(s,
"org.apache.brooklyn.core.workflow.WorkflowExecutionContext$Body.call");
+ Asserts.assertStringDoesNotContain(s, "\"errorRecord\":");
+ wc = json.readValue(s, WorkflowExecutionContext.class);
+ Asserts.assertNotNull(wc.getEntity());
+
Asserts.assertEquals(wc.getCurrentStepInstance().getError().getClass(),
RuntimeException.class);
+ Asserts.assertNull(wc.getCurrentStepInstance().getError().getCause());
+
+ // write
+ wc = WorkflowBasicTest.runWorkflow(app, "steps: [ fail message Testing
failure ]", "test-failure");
+ wc.getTaskSkippingCondition().get().blockUntilEnded();
+ Asserts.assertNotNull(wc.getEntity());
+
Asserts.assertNotNull(wc.getCurrentStepInstance().getError().getCause());
+
+ /* the 'error' field, with its trace, is no longer persisted via
xstream; we store a simpler record instead.
+ * the reason for this is that many errors include context objects
which don't serialize well and cannot otherwise be intercepted.
+ * the exception is if the exception implement serializable. */
+ sw = new StringWriter();
+ xml.serialize(wc, sw);
+ Asserts.assertStringDoesNotContain(sw.toString(), "<error ");
+ Asserts.assertStringDoesNotContain(sw.toString(),
"<trace>org.apache.brooklyn.core.workflow.WorkflowExecutionContext$Body.call");
+ wc = (WorkflowExecutionContext) xml.deserialize(new
StringReader(sw.toString()));
+ Asserts.assertNotNull(wc.getEntity());
+
Asserts.assertEquals(wc.getCurrentStepInstance().getError().getClass(),
RuntimeException.class);
+ Asserts.assertNull(wc.getCurrentStepInstance().getError().getCause());
+ }
+
+}
diff --git
a/core/src/test/java/org/apache/brooklyn/util/core/xstream/XmlSerializerTest.java
b/core/src/test/java/org/apache/brooklyn/util/core/xstream/XmlSerializerTest.java
index e2c5b950e9..b3c38d24b8 100644
---
a/core/src/test/java/org/apache/brooklyn/util/core/xstream/XmlSerializerTest.java
+++
b/core/src/test/java/org/apache/brooklyn/util/core/xstream/XmlSerializerTest.java
@@ -25,6 +25,8 @@ import
com.thoughtworks.xstream.converters.basic.BooleanConverter;
import com.thoughtworks.xstream.converters.extended.ToAttributedValueConverter;
import java.io.Serializable;
import java.util.Arrays;
+import java.util.Map;
+import java.util.function.Consumer;
import java.util.function.Supplier;
import org.apache.brooklyn.test.Asserts;
import org.apache.brooklyn.util.collections.MutableList;
@@ -32,6 +34,7 @@ import org.apache.brooklyn.util.collections.MutableMap;
import org.apache.brooklyn.util.collections.MutableSet;
import org.apache.brooklyn.util.core.config.ConfigBag;
import
org.apache.brooklyn.util.core.xstream.LambdaPreventionMapper.LambdaPersistenceMode;
+import org.apache.brooklyn.util.exceptions.Exceptions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.testng.Assert.assertEquals;
@@ -145,9 +148,13 @@ public class XmlSerializerTest {
Asserts.assertNull(s2);
}
private void serializeExpectingFailure(Supplier<String> s) {
+ serializeExpectingFailure(s, null);
+ }
+ private void serializeExpectingFailure(Object s, Consumer<Throwable>
...otherChecks) {
Asserts.assertFailsWith(()->serializer.toString(s),
error -> {
Asserts.expectedFailureContainsIgnoreCase(error, "lambda");
+ if (otherChecks!=null)
Arrays.asList(otherChecks).forEach(c -> c.accept(error));
return true;
});
}
@@ -173,8 +180,33 @@ public class XmlSerializerTest {
@Test
public void testLambdaXstreamFailingAllSerializable() throws Exception {
serializer = new XmlSerializer<Object>(null, null,
LambdaPreventionMapper.factory(ConfigBag.newInstance().configure(LambdaPreventionMapper.LAMBDA_PERSISTENCE,
LambdaPersistenceMode.FAIL)));
- serializeExpectingFailure( () -> "hello" );
- serializeExpectingFailure( (SerializableSupplier<String>) () ->
"hello" );
+ serializeExpectingFailure(() -> "hello");
+ serializeExpectingFailure((SerializableSupplier<String>) () ->
"hello");
+ serializeExpectingFailure(MutableMap.of("key", (Supplier) () ->
"hello"), e -> Asserts.expectedFailureContainsIgnoreCase(e, "MutableMap/key"));
+ }
+
+ @Test(groups="WIP")
+ public void testLambdaXstreamFailingWithNonSerializableException() throws
Exception {
+ SafeThrowableConverter.TODO++;
+
+ // this test passes but the fact it comes back as object is
problematic; if the field needs a supplier or something else, it won't
deserialize.
+
+ serializer = new XmlSerializer<Object>(null, null,
LambdaPreventionMapper.factory(ConfigBag.newInstance().configure(LambdaPreventionMapper.LAMBDA_PERSISTENCE,
LambdaPersistenceMode.FAIL)));
+ String safelyTidiedException =
serializer.toString(MutableMap.of("key", new RuntimeException("some exception",
+ new TestExceptionWithContext("wrapped exception", null,
(Supplier) () -> "hello"))));
+ Object tidiedException = serializer.fromString(safelyTidiedException);
+ Throwable e1 = (Throwable) ((Map)tidiedException).get("key");
+ Throwable e2 = e1.getCause();
+ Asserts.assertEquals("wrapped exception", e2.getMessage());
+ Asserts.assertThat(((TestExceptionWithContext)e2).context, x ->
x==null || x.getClass().equals(Object.class));
+ }
+
+ public static class TestExceptionWithContext extends Exception {
+ final Object context;
+ public TestExceptionWithContext(String msg, Throwable cause, Object
context) {
+ super(msg, cause);
+ this.context = context;
+ }
}
@Test
diff --git
a/core/src/test/resources/org/apache/brooklyn/core/workflow/workflow-with-error-with-trace.xml
b/core/src/test/resources/org/apache/brooklyn/core/workflow/workflow-with-error-with-trace.xml
new file mode 100644
index 0000000000..e64bc57654
--- /dev/null
+++
b/core/src/test/resources/org/apache/brooklyn/core/workflow/workflow-with-error-with-trace.xml
@@ -0,0 +1,145 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright 2015 The Apache Software Foundation.
+
+Licensed 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.
+-->
+<org.apache.brooklyn.core.workflow.WorkflowExecutionContext>
+ <name>test-failure</name>
+ <entity>__ENTITY_ID__</entity>
+ <status>ERROR</status>
+ <lastStatusChangeTime>2023-08-07T19:29:29.778Z</lastStatusChangeTime>
+ <stepsDefinition>
+ <string>fail message Testing failure</string>
+ </stepsDefinition>
+ <input class="MutableMap"/>
+ <inputResolved class="MutableMap"/>
+ <onError class="MutableList"/>
+ <workflowId>Zt1C3W5k</workflowId>
+ <taskId>Zt1C3W5k</taskId>
+ <retention>
+ <expiryResolved>parent</expiryResolved>
+ </retention>
+ <replays class="MutableSet">
+
<org.apache.brooklyn.core.workflow.WorkflowReplayUtils_-WorkflowReplayRecord>
+ <taskId>Zt1C3W5k</taskId>
+ <reasonForReplay>initial run</reasonForReplay>
+ <submitTimeUtc>1691436569686</submitTimeUtc>
+ <startTimeUtc>1691436569686</startTimeUtc>
+ <endTimeUtc>1691436569779</endTimeUtc>
+ <status>Failed</status>
+ <isError>true</isError>
+ <result class="string">WorkflowFailException: Testing
failure</result>
+
</org.apache.brooklyn.core.workflow.WorkflowReplayUtils_-WorkflowReplayRecord>
+ </replays>
+ <currentStepIndex>0</currentStepIndex>
+ <currentStepInstance>
+ <stepIndex>0</stepIndex>
+ <taskId>a1DHbS0O</taskId>
+ <input class="MutableMap">
+ <rethrow type="boolean">false</rethrow>
+ <message>Testing failure</message>
+ </input>
+ <inputResolved class="MutableMap"/>
+ <error
class="org.apache.brooklyn.util.exceptions.PropagatedRuntimeException">
+ <detailMessage></detailMessage>
+ <cause class="java.util.concurrent.ExecutionException">
+
<detailMessage>org.apache.brooklyn.core.workflow.steps.flow.FailWorkflowStep$WorkflowFailException:
Testing failure</detailMessage>
+ <cause
class="org.apache.brooklyn.core.workflow.steps.flow.FailWorkflowStep$WorkflowFailException">
+ <detailMessage>Testing failure</detailMessage>
+ <stackTrace>
+
<trace>org.apache.brooklyn.core.workflow.steps.flow.FailWorkflowStep.doTaskBody(FailWorkflowStep.java:55)</trace>
+
<trace>org.apache.brooklyn.core.workflow.WorkflowStepDefinition.lambda$null$1(WorkflowStepDefinition.java:226)</trace>
+
<trace>org.apache.brooklyn.core.workflow.WorkflowStepDefinition.lambda$newTask$2(WorkflowStepDefinition.java:230)</trace>
+
<trace>org.apache.brooklyn.util.core.task.DynamicSequentialTask$DstJob.call(DynamicSequentialTask.java:382)</trace>
+
<trace>org.apache.brooklyn.util.core.task.BasicExecutionManager$SubmissionCallable.call(BasicExecutionManager.java:910)</trace>
+
<trace>java.util.concurrent.FutureTask.run$$$capture(FutureTask.java:266)</trace>
+
<trace>java.util.concurrent.FutureTask.run(FutureTask.java)</trace>
+
<trace>java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)</trace>
+
<trace>java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)</trace>
+ <trace>java.lang.Thread.run(Thread.java:750)</trace>
+ </stackTrace>
+ <suppressedExceptions
class="java.util.Collections$UnmodifiableRandomAccessList"
resolves-to="java.util.Collections$UnmodifiableList">
+ <c class="list"/>
+ <list reference="../c"/>
+ </suppressedExceptions>
+ </cause>
+ <stackTrace>
+
<trace>java.util.concurrent.FutureTask.report(FutureTask.java:122)</trace>
+
<trace>java.util.concurrent.FutureTask.get(FutureTask.java:192)</trace>
+
<trace>com.google.common.util.concurrent.ForwardingFuture.get(ForwardingFuture.java:62)</trace>
+
<trace>org.apache.brooklyn.util.core.task.BasicTask.get(BasicTask.java:384)</trace>
+
<trace>org.apache.brooklyn.util.core.task.BasicTask.getUnchecked(BasicTask.java:393)</trace>
+
<trace>org.apache.brooklyn.core.workflow.WorkflowExecutionContext$Body.runCurrentStepInstanceApproved(WorkflowExecutionContext.java:1526)</trace>
+
<trace>org.apache.brooklyn.core.workflow.WorkflowExecutionContext$Body.runCurrentStepIfPreconditions(WorkflowExecutionContext.java:1434)</trace>
+
<trace>org.apache.brooklyn.core.workflow.WorkflowExecutionContext$Body.callSteps(WorkflowExecutionContext.java:1099)</trace>
+
<trace>org.apache.brooklyn.core.workflow.WorkflowExecutionContext$Body.callWithLock(WorkflowExecutionContext.java:1029)</trace>
+
<trace>org.apache.brooklyn.core.workflow.WorkflowExecutionContext$Body.call(WorkflowExecutionContext.java:951)</trace>
+
<trace>org.apache.brooklyn.util.core.task.DynamicSequentialTask$DstJob.call(DynamicSequentialTask.java:382)</trace>
+
<trace>org.apache.brooklyn.util.core.task.BasicExecutionManager$SubmissionCallable.call(BasicExecutionManager.java:910)</trace>
+
<trace>java.util.concurrent.FutureTask.run$$$capture(FutureTask.java:266)</trace>
+
<trace>java.util.concurrent.FutureTask.run(FutureTask.java)</trace>
+
<trace>java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)</trace>
+
<trace>java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)</trace>
+ <trace>java.lang.Thread.run(Thread.java:750)</trace>
+ </stackTrace>
+ <suppressedExceptions
class="java.util.Collections$UnmodifiableRandomAccessList"
reference="../cause/suppressedExceptions"/>
+ </cause>
+ <stackTrace>
+
<trace>org.apache.brooklyn.util.exceptions.Exceptions.propagate(Exceptions.java:128)</trace>
+
<trace>org.apache.brooklyn.util.core.task.BasicTask.getUnchecked(BasicTask.java:395)</trace>
+
<trace>org.apache.brooklyn.core.workflow.WorkflowExecutionContext$Body.runCurrentStepInstanceApproved(WorkflowExecutionContext.java:1526)</trace>
+
<trace>org.apache.brooklyn.core.workflow.WorkflowExecutionContext$Body.runCurrentStepIfPreconditions(WorkflowExecutionContext.java:1434)</trace>
+
<trace>org.apache.brooklyn.core.workflow.WorkflowExecutionContext$Body.callSteps(WorkflowExecutionContext.java:1099)</trace>
+
<trace>org.apache.brooklyn.core.workflow.WorkflowExecutionContext$Body.callWithLock(WorkflowExecutionContext.java:1029)</trace>
+
<trace>org.apache.brooklyn.core.workflow.WorkflowExecutionContext$Body.call(WorkflowExecutionContext.java:951)</trace>
+
<trace>org.apache.brooklyn.util.core.task.DynamicSequentialTask$DstJob.call(DynamicSequentialTask.java:382)</trace>
+
<trace>org.apache.brooklyn.util.core.task.BasicExecutionManager$SubmissionCallable.call(BasicExecutionManager.java:910)</trace>
+
<trace>java.util.concurrent.FutureTask.run$$$capture(FutureTask.java:266)</trace>
+
<trace>java.util.concurrent.FutureTask.run(FutureTask.java)</trace>
+
<trace>java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)</trace>
+
<trace>java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)</trace>
+ <trace>java.lang.Thread.run(Thread.java:750)</trace>
+ </stackTrace>
+ <suppressedExceptions
class="java.util.Collections$UnmodifiableRandomAccessList"
reference="../cause/cause/suppressedExceptions"/>
+ <causeEmbeddedInMessage>false</causeEmbeddedInMessage>
+ </error>
+ <subWorkflows class="MutableSet"/>
+ <otherMetadata class="MutableMap"/>
+ </currentStepInstance>
+ <oldStepInfo class="MutableMap">
+ <entry>
+ <int>0</int>
+
<org.apache.brooklyn.core.workflow.WorkflowExecutionContext_-OldStepRecord>
+ <countStarted>1</countStarted>
+ <countCompleted>0</countCompleted>
+ <context reference="../../../../currentStepInstance"/>
+ <previous class="MutableSet">
+ <int>-1</int>
+ </previous>
+
</org.apache.brooklyn.core.workflow.WorkflowExecutionContext_-OldStepRecord>
+ </entry>
+ <entry>
+ <int>-1</int>
+
<org.apache.brooklyn.core.workflow.WorkflowExecutionContext_-OldStepRecord>
+ <countStarted>0</countStarted>
+ <countCompleted>0</countCompleted>
+ <next class="MutableSet">
+ <int>0</int>
+ </next>
+ <nextTaskId>a1DHbS0O</nextTaskId>
+
</org.apache.brooklyn.core.workflow.WorkflowExecutionContext_-OldStepRecord>
+ </entry>
+ </oldStepInfo>
+ <retryRecords class="MutableMap"/>
+</org.apache.brooklyn.core.workflow.WorkflowExecutionContext>
\ No newline at end of file
diff --git
a/utils/common/src/main/java/org/apache/brooklyn/util/exceptions/LossySerializingThrowable.java
b/utils/common/src/main/java/org/apache/brooklyn/util/exceptions/LossySerializingThrowable.java
new file mode 100644
index 0000000000..dee6884f18
--- /dev/null
+++
b/utils/common/src/main/java/org/apache/brooklyn/util/exceptions/LossySerializingThrowable.java
@@ -0,0 +1,50 @@
+/*
+ * 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.brooklyn.util.exceptions;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+
+/**
+ * Wraps a throwable in something which does not serialize the original, but
which does preserve the message after serialization.
+ */
+public class LossySerializingThrowable extends RuntimeException {
+
+ transient Throwable error;
+ String type;
+
+ // jackson constructor
+ private LossySerializingThrowable() {}
+
+ public LossySerializingThrowable(Throwable error) {
+ super(Exceptions.collapseText(error), null, false, false);
+ this.error = error;
+ this.type = error.getClass().getCanonicalName();
+ }
+
+ @JsonIgnore
+ public Throwable getError() {
+ if (error!=null) return error;
+ return new RuntimeException(getMessage()!=null ? getMessage() : type);
+ }
+
+ @JsonIgnore
+ public String getType() {
+ return type;
+ }
+}