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;
+    }
+}

Reply via email to