All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.mats3.serial.impl.MatsTraceFieldImpl Maven / Gradle / Ivy

Go to download

Mats^3 wire format solution called "MatsTrace", which defines a set of parameters and structures sufficient to represent an envelope carrying Mats messages, as well as a deser-interface "MatsSerializer" which defines methods between MatsTrace and byte arrays. Employed by the Mats^3 JMS Implementation.

There is a newer version: 0.19.19-2024-04-30
Show newest version
package io.mats3.serial.impl;

import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.TimeZone;
import java.util.concurrent.ThreadLocalRandom;

import io.mats3.serial.MatsTrace;
import io.mats3.serial.MatsTrace.Call.CallType;
import io.mats3.serial.MatsTrace.Call.Channel;
import io.mats3.serial.MatsTrace.Call.MessagingModel;

/**
 * An implementation of {@link MatsTrace} which uses fields to hold all state necessary for a Mats flow, including
 * "holders" for the serialization of DTOs and STOs, with type 'Z'. It is meant to be "field-serialized", thus the field
 * names are short. The most relevant types of Z are String and byte[], using e.g. JSON or Smile for serializing the
 * DTOs and STOs payloads, but it might be relevant to use a container/holder type too. You should create an extension
 * of this class to set the type. In 'mats-serial-json', a MatsTraceStringImpl variant exists.
 *
 * @author Endre Stølsvik - 2015 - http://endre.stolsvik.com
 */
public class MatsTraceFieldImpl implements MatsTrace, Cloneable {

    private final String id; // "Flow Id", system-def Id for this call flow (as oppose to traceId, which is user def.)
    private final String tid; // TraceId, user-def Id for this call flow.

    private long ts; // Initialized @ TimeStamp (Java epoch). Not final due to legacy withDebugInfo(..)

    private final KeepMatsTrace kt; // KeepMatsTrace.
    private final Boolean np; // NonPersistent.
    private final Boolean ia; // Interactive.
    private final Long tl; // Time-To-Live, null if 0, where 0 means "forever".
    private final Boolean na; // NoAudit.

    private Long tidh; // For future OpenTracing support: 16-byte TraceId HIGH
    private Long tidl; // For future OpenTracing support: 16-byte TraceId LOW
    private Long sid; // For future OpenTracing support: Override SpanId for root
    private Long pid; // For future OpenTracing support: ParentId (note: "ChildOf" in spec)
    private Byte f; // For future OpenTracing support: Flags
    private long[] sids; // Open tracing SpanId stack. Not currently used, might be a better implementation.

    private int d; // For future Debug options, issue #79

    private String an; // Initializing AppName
    private String av; // Initializing AppVersion
    private String h; // Initializing Host/Node
    private String iid; // Initiator Id, "from" on initiation
    private String x; // Debug info (free-form..)

    private String auth; // For future Auth support: Initializing Authorization header, e.g. "Bearer: ....".

    private String sig; // For future Signature support: Signature of central pieces of information in the trace.
    // Note regarding signature: This is meant for the initial elements of the trace, kept in the trace.
    // The entire message is also signed, and the signature is kept in byte-sideloads.

    private int cn; // Call Number. Not final due to clone impl. Mutable; increases for each message passing in flow.
    private int tcn; // For "StackOverflow" detector: "Total Call Number", does not reset when initiation within stage.
    private String pmid; // If initiated within a flow (stage): Parent MatsMessageId.

    private List> c = new ArrayList<>(); // Calls, "Call Flow". Not final due to clone-impl.
    private List> ss = new ArrayList<>(); // StackStates, "State Flow". Not final due to clone-impl.
    private Map tp = new LinkedHashMap<>(); // TraceProps. Not final due to clone-impl.

    private long[] ots; // Outgoing timestamp ("same height"): "compressed" against the initiation time
    private long[] eets; // Endpoint Entered timestamp: "compressed" against the initiation time

    @Override
    public MatsTrace withDebugInfo(String initializingAppName, String initializingAppVersion,
            String initializingHost, String initiatorId, String debugInfo) {
        an = initializingAppName;
        av = initializingAppVersion;
        h = initializingHost;
        iid = initiatorId;
        x = debugInfo;
        return this;
    }

    @Override
    public MatsTrace withChildFlow(String parentMatsMessageId, int totalCallNumber) {
        pmid = parentMatsMessageId;
        tcn = totalCallNumber;
        return this;
    }

    // TODO: POTENTIAL withOpenTracingTraceId() and withOpenTracingSpanId()..

    // Jackson JSON-lib needs a no-args constructor, but it can re-set finals.
    protected MatsTraceFieldImpl() {
        // REMEMBER: These will be set by the deserialization mechanism.
        tid = null;
        id = null;

        kt = null;
        np = null;
        ia = null;
        tl = null;
        na = null;
    }

    protected MatsTraceFieldImpl(String traceId, String flowId, KeepMatsTrace keepMatsTrace, boolean nonPersistent,
            boolean interactive, long ttlMillis, boolean noAudit) {
        this.tid = traceId;
        this.id = flowId;
        // This should really have been provided by user, when the initiation was /started/.
        this.ts = System.currentTimeMillis();
        // Set the initialization timestamp as "endpoint entered", so that if the init is a REQUEST, you can get
        // the "total endpoint time" on the terminator, as init-to-terminator.
        // The 'eets' (and 'ots') uses diff-from-initialization.
        this.eets = new long[] { 0 };
        this.kt = keepMatsTrace;
        this.np = nonPersistent ? Boolean.TRUE : null;
        this.ia = interactive ? Boolean.TRUE : null;
        this.tl = ttlMillis > 0 ? ttlMillis : null;
        this.na = noAudit ? Boolean.TRUE : null;
        this.cn = 0;
        this.tcn = 0;
    }

    /**
     * NOTICE! This is NOT meant for public usage!
     */
    public void overrideInitializationTimestamp(long timestamp) {
        this.ts = timestamp;
        // Set the initialization timestamp as "endpoint entered", so that if the init is a REQUEST, you can get
        // the "total endpoint time" on the terminator, as init-to-terminator.
        // The 'eets' (and 'ots') uses diff-from-initialization.
        this.eets = new long[] { 0 };
    }

    // == NOTICE == Serialization and deserialization is an implementation specific feature.

    @Override
    public String getTraceId() {
        return tid;
    }

    @Override
    public String getFlowId() {
        return id;
    }

    @Override
    public long getInitializedTimestamp() {
        return ts;
    }

    @Override
    public KeepMatsTrace getKeepTrace() {
        return kt;
    }

    @Override
    public boolean isNonPersistent() {
        return np == null ? Boolean.FALSE : np;
    }

    @Override
    public boolean isInteractive() {
        return ia == null ? Boolean.FALSE : ia;
    }

    @Override
    public long getTimeToLive() {
        return tl != null ? tl : 0;
    }

    @Override
    public boolean isNoAudit() {
        return na == null ? Boolean.FALSE : na;
    }

    @Override
    public String getInitializingAppName() {
        return an == null ? NULLED : an;
    }

    @Override
    public String getInitializingAppVersion() {
        return av == null ? NULLED : av;
    }

    @Override
    public String getInitializingHost() {
        return h == null ? NULLED : h;
    }

    @Override
    public String getInitiatorId() {
        return iid == null ? NULLED : iid;
    }

    @Override
    public String getDebugInfo() {
        return x;
    }

    @Override
    public int getCallNumber() {
        return cn;
    }

    @Override
    public int getTotalCallNumber() {
        return tcn;
    }

    @Override
    public String getParentMatsMessageId() {
        return pmid;
    }

    @Override
    public void setTraceProperty(String propertyName, Z propertyValue) {
        tp.put(propertyName, propertyValue);
    }

    @Override
    public Z getTraceProperty(String propertyName) {
        return tp.get(propertyName);
    }

    @Override
    public Set getTracePropertyKeys() {
        return tp.keySet();
    }

    @Override
    public MatsTraceFieldImpl addRequestCall(String from,
            String to, MessagingModel toMessagingModel,
            String replyTo, MessagingModel replyToMessagingModel,
            Z data, Z replyState, Z initialState) {
        // Get copy of current stack. We're going to add a stack frame to it.
        List newCallReplyStack = getCopyOfCurrentStackForNewCall();
        // Clone the current MatsTrace, which is the one we're going to modify and return.
        MatsTraceFieldImpl clone = cloneForNewCall();
        // :: Add the replyState - i.e. the state that is outgoing from the current stage, destined for the REPLY.
        // NOTE: This must be added BEFORE we add to the newCallReplyStack, since it is targeted to the stack frame
        // below this new Request stack frame!
        StackStateImpl newState = new StackStateImpl(newCallReplyStack.size(), replyState);
        // NOTE: Extra-state that was added from a previous message passing must be kept. We must thus copy that.
        // Get current StackStateImpl - before adding new to reply stack. CAN BE NULL, both if initial stage, or no
        // extra state added yet. Take into account if this the very first call.
        forwardExtraStateIfExist(newState);
        // Actually add the new state
        clone.ss.add(newState);

        // Add the stageId to replyTo to the stack
        newCallReplyStack.add(ReplyChannel.newWithRandomSpanId(replyTo, replyToMessagingModel));
        // Prune the data and stack from current call if KeepMatsTrace says so.
        clone.dropValuesOnCurrentCallIfAny();
        // Add the new Call
        clone.c.add(new CallImpl(CallType.REQUEST, getFlowId(), getInitializedTimestamp(), getCallNumber(), from,
                new ToChannel(to, toMessagingModel), data, newCallReplyStack));
        // Add any state meant for the initial stage ("stage0") of the "to" endpointId.
        if (initialState != null) {
            // The stack is now one height higher, since we added the "replyTo" to it.
            clone.ss.add(new StackStateImpl(newCallReplyStack.size(), initialState));
        }
        // Prune the StackStates if KeepMatsTrace says so
        clone.pruneUnnecessaryStackStates();
        return clone;
    }

    @Override
    public MatsTraceFieldImpl addSendCall(String from, String to, MessagingModel toMessagingModel,
            Z data, Z initialState) {
        // Get copy of current stack. NOTE: For a send/next call, the stack does not change.
        List newCallReplyStack = getCopyOfCurrentStackForNewCall();
        // Clone the current MatsTrace, which is the one we're going to modify and return.
        MatsTraceFieldImpl clone = cloneForNewCall();
        // Prune the data and stack from current call if KeepMatsTrace says so.
        clone.dropValuesOnCurrentCallIfAny();
        // Add the new Call
        clone.c.add(new CallImpl(CallType.SEND, getFlowId(), getInitializedTimestamp(), getCallNumber(), from,
                new ToChannel(to, toMessagingModel), data, newCallReplyStack));
        // Add any state meant for the initial stage ("stage0") of the "to" endpointId.
        if (initialState != null) {
            clone.ss.add(new StackStateImpl(newCallReplyStack.size(), initialState));
        }
        // Prune the StackStates if KeepMatsTrace says so.
        clone.pruneUnnecessaryStackStates();
        return clone;
    }

    @Override
    public MatsTraceFieldImpl addNextCall(String from, String to, Z data, Z state) {
        if (state == null) {
            throw new IllegalStateException("When adding next-call, state-data string should not be null.");
        }
        // Get copy of current stack. NOTE: For a send/next call, the stack does not change.
        List newCallReplyStack = getCopyOfCurrentStackForNewCall();
        // Clone the current MatsTrace, which is the one we're going to modify and return.
        MatsTraceFieldImpl clone = cloneForNewCall();
        // Prune the data and stack from current call if KeepMatsTrace says so.
        clone.dropValuesOnCurrentCallIfAny();
        // Add the new Call.
        clone.c.add(new CallImpl(CallType.NEXT, getFlowId(), getInitializedTimestamp(), getCallNumber(), from,
                new ToChannel(to, MessagingModel.QUEUE), data, newCallReplyStack));
        // Add the state meant for the next stage (Notice again that we do not change the reply stack here)
        StackStateImpl newState = new StackStateImpl(newCallReplyStack.size(), state);
        // NOTE: Extra-state that was added from a previous message passing must be kept. We must thus copy that.
        forwardExtraStateIfExist(newState);
        // Actually add the new state
        clone.ss.add(newState);
        // Prune the StackStates if KeepMatsTrace says so.
        clone.pruneUnnecessaryStackStates();
        return clone;
    }

    @Override
    public MatsTraceFieldImpl addReplyCall(String from, Z data) {
        // Get copy of current stack. We're going to pop a stack frame of it.
        List newCallReplyStack = getCopyOfCurrentStackForNewCall();
        // ?: Do we actually have anything to pop?
        if (newCallReplyStack.size() == 0) {
            // -> No stack: Illegal - you shouldn't be making a REPLY call if there is nothing to reply to.
            throw new IllegalStateException("Trying to add Reply Call when there is no stack."
                    + " (Implementation note: You need to check the getCurrentCall().getStackHeight() before trying to"
                    + " do a reply - if it is zero, then just drop the reply instead.)");
        }
        // Clone the current MatsTrace, which is the one we're going to modify and return.
        MatsTraceFieldImpl clone = cloneForNewCall();
        // Prune the data and stack from current call if KeepMatsTrace says so.
        clone.dropValuesOnCurrentCallIfAny();
        // Pop the last element off the stack, since this is where we'll reply to, and the rest is the new stack.
        ReplyChannel to = newCallReplyStack.remove(newCallReplyStack.size() - 1);
        // Add the new Call, adding the ReplyForSpanId.
        CallImpl replyCall = new CallImpl(CallType.REPLY, getFlowId(), getInitializedTimestamp(), getCallNumber(),
                from, new ToChannel(to.i, to.m), data, newCallReplyStack).setReplyForSpanId(getCurrentSpanId());
        clone.c.add(replyCall);
        // Prune the StackStates if KeepMatsTrace says so.
        clone.pruneUnnecessaryStackStates();
        return clone;
    }

    @Override
    public MatsTrace addGotoCall(String from, String to, Z data, Z initialState) {
        // Get copy of current stack. NOTE: For a send/next/pass call, the stack does not change.
        List newCallReplyStack = getCopyOfCurrentStackForNewCall();
        // Clone the current MatsTrace, which is the one we're going to modify and return.
        MatsTraceFieldImpl clone = cloneForNewCall();
        // Prune the data and stack from current call if KeepMatsTrace says so.
        clone.dropValuesOnCurrentCallIfAny();
        // Add the new Call.
        clone.c.add(new CallImpl(CallType.GOTO, getFlowId(), getInitializedTimestamp(), getCallNumber(), from,
                new ToChannel(to, MessagingModel.QUEUE), data, newCallReplyStack));
        if (initialState != null) {
            // Add the state meant for the goto'ed endpoint (Notice again that we do not change the reply stack here)
            StackStateImpl initialGototate = new StackStateImpl(newCallReplyStack.size(), initialState);
            // Actually add the new state
            clone.ss.add(initialGototate);
        }
        // Prune the StackStates if KeepMatsTrace says so.
        clone.pruneUnnecessaryStackStates();
        return clone;
    }

    @Override
    public void setOutgoingTimestamp(long timestamp) {
        // NOTE: SENDING SIDE: Invoked when a new message/call has been constructed, about to be sent.
        // NOTE: Shall be invoked AFTER having added a call for new outgoing message, as late as possible before send.

        // :: Set on the outgoing call
        CallImpl cc = getCurrentCall();
        cc.setCalledTimestamp(timestamp);

        // :: Handle the "outgoing timestamp stack"
        // NOTE: This enables the "time between stages" calculation: From send, to receive /on same stack height/.

        int stackHeight = cc.getReplyStackHeight();

        // ?: Is this a REPLY?
        if (cc.t == CallType.REPLY) {
            // -> Yes, REPLY: Then we should not set a new timestamp, but crop off the existing height
            // If we /were/ at stack height 2, then there /was/ 3 timestamps (height 0, 1, 2).
            // The current height (due to REPLY) is now 1, so now there should be 2 timestamps (height 0, 1)
            // Copy the current, cropping if necessary, extending if necessary. Must handle null due to old impls.
            this.ots = this.ots == null ? new long[stackHeight + 1] : Arrays.copyOf(this.ots, stackHeight + 1);
            return;
        }

        // Find difference between initiation and timestamp
        long diff = (timestamp - getInitializedTimestamp());

        // ?: Is this a REQUEST?
        if (cc.t == CallType.REQUEST) {
            // -> Yes, REQUEST. Then we should set our timestamp on the stackheight /below/ this call.
            // If we /were/ at stack height 1, then there /was/ 2 timestamps (height 0, 1).
            // The current height (due to REQUEST) is now 2, but we should leave our timestamp at height 1.
            // Copy the current, cropping if necessary, extending if necessary. Must handle null due to old impls.
            this.ots = this.ots == null ? new long[stackHeight] : Arrays.copyOf(this.ots, stackHeight);
            this.ots[stackHeight - 1] = diff;
            return;
        }

        // E-> This is a SEND or PUBLISH (initiations), or NEXT or GOTO (flows)

        // If we /were/ at stack height 1, then there /was/ 2 timestamps (height 0, 1).
        // The current height is still now 1, and there is still 2 timestamps (height 0, 1),
        // and we should leave our timestamp at height 1.
        // Copy the current, cropping if necessary, extending if necessary. Must handle null due to old impls.
        this.ots = this.ots == null ? new long[stackHeight + 1] : Arrays.copyOf(this.ots, stackHeight + 1);
        this.ots[stackHeight] = diff;
    }

    @Override
    public long getSameHeightOutgoingTimestamp() {
        // NOTE: RECEIVING SIDE: Invoked when a message has been received
        CallImpl cc = getCurrentCall();
        // ?: Is the current call a REQUEST?
        if (cc.t == CallType.REQUEST) {
            // -> Yes, CC is a REQUEST. There is per definition then no "same height" in front.
            // Should really throw, as this makes no sense. Returning -1 instead, hope caller realizes his mistake.
            return -1;
        }
        // E-> Not REQUEST

        // For all types (outside of the REQUEST handled above), we should use the current stack height as the index of
        // where to find the "same height outgoing timestamp" that was set on the sender.

        int stackHeight = getCurrentCall().getReplyStackHeight();
        // Must handle null due to old impls.
        return this.ots == null ? 0 : this.ots[stackHeight] + getInitializedTimestamp();
    }

    @Override
    public void setStageEnteredTimestamp(long timestamp) {
        // NOTE: RECEIVING SIDE
        // NOTE: Shall be invoked RIGHT WHEN RECEIVING a message on a stage

        // :: Calculating EndpointEntered time, thus only when being received on the INITIAL stage

        CallImpl cc = getCurrentCall();

        // ?: Is this a NEXT?
        if (cc.t == CallType.NEXT) {
            // -> Yes NEXT. This is per definition not an *initial* stage.
            // The endpoint-entered time stays the same - thus not touching the endpoint-entered stack.
            return;
        }

        // ?: Is this a GOTO?
        if (cc.t == CallType.GOTO) {
            // -> Yes GOTO. This will be a new initial stage, but since it is a GOTO, it should not reset the
            // endpoint-entered timestamp - thus not touching the endpoint-entered stack.
            return;
        }

        // ?: Is this a REPLY?
        if (cc.t == CallType.REPLY) {
            // -> Yes, REPLY. This cannot per definition be an *initial* stage.
            // The endpoint-entered stack was cropped when the message was sent, so not touching it.
            return;
        }

        // E-> This is a SEND or PUBLISH (initiation), or a REQUEST (flow)

        int stackHeight = cc.getReplyStackHeight();

        // Find difference between initiation and timestamp
        long diff = (timestamp - getInitializedTimestamp());

        // If we are at stack height 1, then there are 2 timestamps in play (height 0, 1).
        // We're the one at stack height 1, so setting our timestamp there
        // Copy the current, cropping if necessary, extending if necessary. Must handle null due to old impls.
        this.eets = this.eets == null ? new long[stackHeight + 1] : Arrays.copyOf(this.eets, stackHeight + 1);
        this.eets[stackHeight] = diff;
    }

    @Override
    public long getSameHeightEndpointEnteredTimestamp() {
        // NOTE: RECEIVING/PROCESSING SIDE
        // NOTE: Shall be invoked on the stage about to either stop flow, or send REPLY.
        // NOTE: BEFORE adding a new call! (Obvious in case of "stopping" a flow, i.e. not sending a new message)

        int stackHeight = getCurrentCall().getReplyStackHeight();

        // If we are at stack height 1, then there are 2 timestamps in play (height 0, 1).
        // We want the one at stack height 1.
        // Must handle null due to old impls.
        return this.eets == null ? 0 : this.eets[stackHeight] + getInitializedTimestamp();
    }

    private void forwardExtraStateIfExist(StackStateImpl newState) {
        // For REQUEST, it might be the very first call in a mats flow - in which case there obviously aren't any
        // current state and extra state yet.
        StackStateImpl currentState = c.isEmpty()
                ? null
                : getState(getCurrentCall().getReplyStackHeight());
        // ?: Do we have a current state, and does that have extra-state?
        if ((currentState != null) && (currentState.es != null)) {
            // -> Yes, there was extra state on the current stage
            // Copy it, and new StackState for the REPLY to the REQUEST we're currently adding.
            newState.es = new HashMap<>(currentState.es);
        }
    }

    /**
     * @return a COPY of the current stack.
     */
    private List getCopyOfCurrentStackForNewCall() {
        if (c.isEmpty()) {
            return new ArrayList<>();
        }
        return getCurrentCall().getReplyStack_internal(); // This is a copy.
    }

    /**
     * Should be invoked just after adding a new Call, so if in non-FULL mode (COMPACT or MINIMAL), we can clean out any
     * stack states that either are higher than we're at now, or multiples for the same height (only the most recent for
     * each stack height is actually a part of the stack, the rest on the same level are for history).
     */
    private void pruneUnnecessaryStackStates() {
        // ?: Are we in MINIMAL or COMPACT modes?
        if ((kt == KeepMatsTrace.MINIMAL) || (kt == KeepMatsTrace.COMPACT)) {
            // -> Yes, so we'll drop the states we can.
            ss = getStateStack_internal();
        }
    }

    // TODO: POTENTIAL setSpanIdOnCurrentStack(..)

    @Override
    public long getCurrentSpanId() {
        // ?: Do we have a CurrentCall?
        CallImpl currentCall = getCurrentCall();
        if (currentCall == null) {
            // -> No, so then we derive the SpanId from the FlowId
            return getRootSpanId();
        }
        // E-> Yes, we have a CurrentCall
        List stack = currentCall.s;
        // ?: Is there any stack?
        if (stack.isEmpty()) {
            // -> No, no stack, so we're at initiator/terminator level - again derive SpanId from FlowId
            return getRootSpanId();
        }
        // E-> Yes, we have a CurrentCall with a Stack > 0 elements.
        return stack.get(stack.size() - 1).getSpanId();
    }

    private long getRootSpanId() {
        return sid != null ? sid : fnv1a_64(getFlowId().getBytes(StandardCharsets.UTF_8));
    }

    private List getSpanIdStack() {
        ArrayList spanIds = new ArrayList<>();
        spanIds.add(getRootSpanId());
        CallImpl currentCall = getCurrentCall();
        // ?: Did we have a CurrentCall?
        if (currentCall != null) {
            // -> We have a CurrentCall, add the stack of SpanIds.
            for (ReplyChannel cws : currentCall.s) {
                spanIds.add(cws.sid);
            }
        }
        return spanIds;
    }

    private static final long FNV1A_64_OFFSET_BASIS = 0xcbf29ce484222325L;
    private static final long FNV1A_64_PRIME = 0x100000001b3L;

    /**
     * Fowler–Noll–Vo hash function, https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function
     */
    private static long fnv1a_64(final byte[] k) {
        long rv = FNV1A_64_OFFSET_BASIS;
        for (byte b : k) {
            rv ^= b;
            rv *= FNV1A_64_PRIME;
        }
        return rv;
    }

    /**
     * Should be invoked just before adding the new call to the cloneForNewCall()'ed MatsTrace, so as to clean out the
     * 'from' and Stack (and data if COMPACT) on the CurrentCall which after the add will become the previous
     * call.
     */
    private void dropValuesOnCurrentCallIfAny() {
        if (c.size() > 0) {
            getCurrentCall().dropFromAndStack();
            // ?: Are we on COMPACT mode? (Note that this is implicitly also done for MINIMAL - in cloneForNewCall() -
            // since all calls are dropped in MINIMAL!)
            if (kt == KeepMatsTrace.COMPACT) {
                // -> Yes, COMPACT, so drop data
                getCurrentCall().dropData();
            }
        }
    }

    @Override
    public CallImpl getCurrentCall() {
        // ?: No calls?
        if (c.size() == 0) {
            // -> No calls, so throw
            throw new IllegalStateException("No calls added - this is evidently a newly created MatsTrace,"
                    + " which isn't meaningful before an initial call is added");
        }
        // Return last element
        return c.get(c.size() - 1);
    }

    @Override
    public List> getCallFlow() {
        return new ArrayList<>(c);
    }

    @Override
    public Optional> getCurrentState() {
        return Optional.ofNullable(getState(getCurrentCall().getReplyStackHeight()));
    }

    @Override
    public List> getStateFlow() {
        return new ArrayList<>(ss);
    }

    @Override
    public List> getStateStack() {
        // heavy-handed hack to get this to conform to the return type.
        @SuppressWarnings({ "unchecked", "rawtypes" })
        List> ret = (List>) (List) getStateStack_internal();
        return ret;
    }

    public List> getStateStack_internal() {
        if (ss.isEmpty()) {
            return new ArrayList<>();
        }
        // Current stack height - stack height is the /position/, not the /size()/.
        int currentCallStackHeight = getCurrentCall().getReplyStackHeight();
        // We want the lowest stack height between the currentCall's stack height, and what is at the last
        // element of the stack (there might not be a StateState for the /current/ stack height yet, only lower,
        // which will happen on every REQUEST that does not have "initial incoming state").
        int topOfStateStack = Math.min(currentCallStackHeight, ss.get(ss.size() - 1).getHeight());
        // Create the return StateStack.
        // Note: the stack height is the /position/, not the /size()/, thus +1 for capacity.
        ArrayList> newStateStack = new ArrayList<>(topOfStateStack + 1);
        // Ensure all positions exist, since we will be traversing backwards when adding
        for (int i = 0; i <= topOfStateStack; i++) {
            newStateStack.add(null);
        }
        // Traverse all the StackStates, keeping the /last/ State at each level.
        for (StackStateImpl stackState : ss) {
            // ?: Is this a State for a stack frame that is /higher/ than we current are on?
            // (Remember the "stack flow", and when we're "going back down" in the stack)
            if (stackState.getHeight() > topOfStateStack) {
                // -> Yes, so we'll not use that
                continue;
            }
            // NOTE: Overwrite! In a FULL stack flow, there might be multiple states for the same stack height.
            // We want the latest for each stack height.
            newStateStack.set(stackState.getHeight(), stackState);
        }
        return newStateStack;
    }

    /**
     * Searches in the stack-list from the back (most recent) for the first element that is of the specified stackDepth.
     * If a more shallow stackDepth than the specified is encountered, or the list is exhausted without the stackDepth
     * being found, the search is terminated with null.
     *
     * @param stackDepth
     *            the stack depth to find stack state for - it should be the size of the stack below you. For e.g. a
     *            Terminator, it is 0. The first request adds a stack level, so it resides at stackDepth 1. Etc.
     * @return the state StackStateImpl if found, null otherwise (as is typical when entering "stage0").
     */
    private StackStateImpl getState(int stackDepth) {
        for (int i = ss.size() - 1; i >= 0; i--) {
            StackStateImpl stackState = ss.get(i);
            // ?: Have we reached a lower depth than ourselves?
            if (stackDepth > stackState.getHeight()) {
                // -> Yes, we're at a lower depth: The rest can not possibly be meant for us.
                break;
            }
            if (stackDepth == stackState.getHeight()) {
                return stackState;
            }
        }
        // Did not find any stack state for us.
        return null;
    }

    /**
     * Takes into account the KeepMatsTrace value.
     */
    protected MatsTraceFieldImpl cloneForNewCall() {
        try {
            @SuppressWarnings("unchecked")
            MatsTraceFieldImpl cloned = (MatsTraceFieldImpl) super.clone();
            // Calls are not immutable (a Call's stack and data may be nulled due to KeepMatsTrace value)
            // ?: Are we using MINIMAL?
            if (kt == KeepMatsTrace.MINIMAL) {
                // -> Yes, MINIMAL, so we will literally just have the sole "NewCall" in the trace.
                cloned.c = new ArrayList<>(1);
            }
            else {
                // -> No, not MINIMAL (i.e. FULL or COMPACT), so clone up the Calls.
                cloned.c = new ArrayList<>(c.size());
                // Clone all the calls.
                for (CallImpl call : c) {
                    cloned.c.add(call.clone());
                }
            }
            // StackStates are mutable (the extra-state)
            cloned.ss = new ArrayList<>(ss.size());
            for (StackStateImpl stateState : ss) {
                cloned.ss.add(stateState.clone());
            }

            // TraceProps are immutable.
            cloned.tp = new LinkedHashMap<>(tp);

            // Increase CallNumber
            cloned.cn = this.cn + 1;
            // Increase TotalCallNumber
            cloned.tcn = this.tcn + 1;
            return cloned;
        }
        catch (CloneNotSupportedException e) {
            throw new AssertionError("Implements Cloneable, so clone() should not throw.", e);
        }
    }

    /**
     * Represents an entry in the {@link MatsTrace}.
     */
    public static class CallImpl implements Call, Cloneable {
        // These four are set by the setDebugInfo, and might be null.
        private String an; // Calling AppName
        private String av; // Calling AppVersion
        private String h; // Calling Host
        private String x; // Debug Info (free-form)

        private long ts; // Calling TimeStamp
        private String id; // MatsMessageId.


        private final CallType t; // type.
        private String f; // from, may be nulled.
        private final ToChannel to; // to.
        private Z d; // data, may be nulled.
        private List s; // stack of reply channels, may be nulled, in which case 'ss' is set.
        private Integer ss; // stack size if stack is nulled.

        private Long rid; // Reply-From-SpanId

        // Jackson JSON-lib needs a no-args constructor, but it can re-set finals.
        private CallImpl() {
            t = null;
            to = null;
        }

        CallImpl(CallType type, String flowId, long matsTraceCreationMillis, int callNo, String from, ToChannel to,
                Z data, List stack) {
            this.t = type;
            this.f = from;
            this.to = to;
            this.d = data;
            this.s = stack;

            this.ts = System.currentTimeMillis();

            // Since we can have clock skews between servers, and we do not want a "-" in the messageId (due to the
            // double-clickableness mentioned below), we make -10 -> "n10".
            long millisSince = this.ts - matsTraceCreationMillis;
            String millisSinceString = millisSince >= 0 ? Long.toString(millisSince) : "n" + Math.abs(millisSince);
            // A MatsMessageId ends up looking like this: 'm_XBExAa1iioAGFVRk6nR5_Tjzswm4ys_t49_n22'
            // Or for negative millisSince: 'm_XBExAa1iioAGFVRk6nR5_Tjzswm4ys_tn49_n22'
            // NOTICE FEATURE: You can double-click anywhere inside that string, and get the entire id marked! w00t!
            // (In all of xterm, browser and IntelliJ - the only non-char that is universally accepted as "part of
            // a string" is actually "_").
            this.id = flowId + "_t" + millisSinceString + "_n" + callNo;
        }

        @Override
        public CallImpl setDebugInfo(String callingAppName, String callingAppVersion, String callingHost,
                String debugInfo) {
            an = callingAppName;
            av = callingAppVersion;
            h = callingHost;
            x = debugInfo;
            return this;
        }

        /**
         * Invoked by {@link MatsTrace#setOutgoingTimestamp(long)}. Resets the calledTimestamp set by constructor, to be
         * more closely timed to the exact sending time. I.e. the message may have been constructed, then a massive SQL
         * query was performed, and then a new message is constructed, and then the messages are actually turned into
         * JMS messages and committed on the wire. This means that the first message will have a much earlier timestamp
         * than the second. Using this method, all outgoing messages can have the Called Timestamp set right
         * before it is serialized and JMS-constructed and committed.
         */
        void setCalledTimestamp(long calledTimestamp) {
            ts = calledTimestamp;
        }

        CallImpl setReplyForSpanId(long replyForSpanId) {
            rid = replyForSpanId;
            return this;
        }

        /**
         * Nulls the "from" and "stack" fields.
         */
        void dropFromAndStack() {
            f = null;
            ss = s.size();
            s = null;
        }

        /**
         * Nulls the "data" field.
         */
        void dropData() {
            d = null;
        }

        @Override
        public String getCallingAppName() {
            return an;
        }

        @Override
        public String getCallingAppVersion() {
            return av;
        }

        @Override
        public String getCallingHost() {
            return h;
        }

        @Override
        public long getCalledTimestamp() {
            return ts;
        }

        @Override
        public String getMatsMessageId() {
            return id;
        }

        @Override
        public String getDebugInfo() {
            return x;
        }

        @Override
        public CallType getCallType() {
            return t;
        }

        @Override
        public long getReplyFromSpanId() {
            if (getCallType() != CallType.REPLY) {
                throw new IllegalStateException("Type of this call is not REPLY, so you cannot ask for"
                        + " ReplyFromSpanId.");
            }
            return rid;
        }

        @Override
        public String getFrom() {
            if (f == null) {
                return NULLED;
            }
            return f;
        }

        @Override
        public Channel getTo() {
            return to;
        }

        @Override
        public Z getData() {
            return d;
        }

        /**
         * @return a COPY of the stack.
         */
        @Override
        public List getReplyStack() {
            // heavy-handed hack to get this to conform to the return type.
            @SuppressWarnings({ "unchecked", "rawtypes" })
            List ret = (List) (List) getReplyStack_internal();
            return ret;
        }

        /**
         * @return a COPY of the stack.
         */
        List getReplyStack_internal() {
            // ?: Has the stack been nulled (to conserve space) due to not being Current Call?
            if (s == null) {
                // -> Yes, nulled, so return a list of correct size where all elements are the string "-nulled-".
                return new ArrayList<>(Collections.nCopies(getReplyStackHeight(),
                        new ReplyChannel(NULLED, null, 0)));
            }
            // E-> No, not nulled (thus Current Call), so return the stack.
            return new ArrayList<>(s);
        }

        @Override
        public int getReplyStackHeight() {
            return (s != null ? s.size() : ss);
        }

        private String indent() {
            return new String(new char[getReplyStackHeight()]).replace("\0", ": ");
        }

        private String fromStackData(boolean printNullData) {
            return "#from:" + (an != null ? an : "") + (av != null ? "[" + av + "]" : "")
                    + (h != null ? "@" + h : "") + (id != null ? ':' + id : "")
                    + (x != null ? ", debug:" + x : "")
                    + (((d != null) || printNullData) ? ", #data:" + d : "");
        }

        @Override
        public String toString() {
            return indent()
                    + t
                    + (ts != 0 ? " " + DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(
                            ZonedDateTime.ofInstant(Instant.ofEpochMilli(ts), TimeZone.getDefault().toZoneId())) + " -"
                            : "")
                    + " #to:" + to
                    + ", " + fromStackData(false);
        }

        public String toStringFromMatsTrace(long startTimestamp, int maxStackSize, int maxToStageIdLength,
                boolean printNulLData) {
            String toType = (ts != 0 ? String.format("%4d", (ts - startTimestamp)) + "ms " : " - ") + indent() + t;
            int numMaxIncludingCallType = 14 + maxStackSize * 2;
            int numSpacesTo = Math.max(0, numMaxIncludingCallType - toType.length());
            String toTo = toType + spaces(numSpacesTo) + " #to:" + to;
            int numSpacesStack = Math.max(1, 7 + numMaxIncludingCallType + maxToStageIdLength - toTo.length());
            return toTo + spaces(numSpacesStack) + fromStackData(printNulLData);
        }

        protected CallImpl clone() {
            try {
                @SuppressWarnings("unchecked")
                CallImpl cloned = (CallImpl) super.clone();
                // Channels are immutable.
                cloned.s = (s == null ? null : new ArrayList<>(s));
                return cloned;
            }
            catch (CloneNotSupportedException e) {
                throw new AssertionError("Implements Cloneable, so clone() should not throw.", e);
            }
        }
    }

    private static String spaces(int length) {
        return new String(new char[length]).replace("\0", " ");
    }

    /**
     * The "standard" implementation of Channel, which is internally only used for the "To" aspect of Channel. For the
     * replyStack, the {@link ReplyChannel} is used.
     */
    private static class ToChannel implements Channel {
        private final String i;
        private final MessagingModel m;

        // Jackson JSON-lib needs a no-args constructor, but it can re-set finals.
        private ToChannel() {
            i = null;
            m = null;
        }

        public ToChannel(String i, MessagingModel m) {
            this.i = i;
            this.m = m;
        }

        @Override
        public String getId() {
            return i;
        }

        @Override
        public MessagingModel getMessagingModel() {
            return m;
        }

        @Override
        public String toString() {
            String model;
            switch (m) {
                case QUEUE:
                    model = "Q";
                    break;
                case TOPIC:
                    model = "T";
                    break;
                default:
                    model = m.toString();
            }
            return "[" + model + "]" + i;
        }
    }

    /**
     * Implementation of {@link Channel} used for the reply stack, extending the {@link ToChannel} by adding SpanId,
     * employed for the {@link CallImpl#getReplyStack_internal()}.
     * 

* We're hitching the SpanIds onto the ReplyTo Stack, as they have the same stack semantics. However, do note that * the Channel-stack and the SpanId-stack are "offset" wrt. to what they refer to: *

    *
  • ReplyTo Stack: The topmost ChannelWithSpan in the stack is what this CurrentCall shall reply to, if it * so desires - i.e. it references the the frame below it in the stack, its parent.
  • *
  • SpanId Stack: The topmost ChannelWithSpan in the stack carries the SpanId that this Call processes * within - i.e. it refers to this frame in the stack.
  • *
* However, when correlating with how OpenTracing and friends refer to SpanIds, these are always created by the * parent - which is also the case here: When a new REQUEST Call is made, this creates a new SpanId (which is kept * with the ReplyChannel that should be replied to) - and then the Call is being sent (inside the MatsTrace). Then, * when the REPLY Call is being created from the requested service, this SpanId is propagated back in the Call, * accessible via the {@link Call#getReplyFromSpanId()} method. Thus, the SpanId is both created, and then processed * again upon receiving the REPLY, by the parent stackframe - and viewed like this, the SpanId ('sid') thus actually * resides on the correct stackframe. */ private static class ReplyChannel implements Channel { private final String i; private final MessagingModel m; private final long sid; // SpanId // Jackson JSON-lib needs a no-args constructor, but it can re-set finals. public ReplyChannel() { i = null; m = null; sid = 0; } public ReplyChannel(String i, MessagingModel m, long sid) { this.i = i; this.m = m; this.sid = sid; } public static ReplyChannel newWithRandomSpanId(String i, MessagingModel m) { return new ReplyChannel(i, m, ThreadLocalRandom.current().nextLong()); } public long getSpanId() { return sid; } @Override public String getId() { return i; } @Override public MessagingModel getMessagingModel() { return m; } } private static class StackStateImpl implements StackState, Cloneable { private final int h; // depth. private final Z s; // state. private Map es; // extraState, map is null until first value present. // Jackson JSON-lib needs a no-args constructor, but it can re-set finals. private StackStateImpl() { h = 0; s = null; } public StackStateImpl(int height, Z state) { this.h = height; this.s = state; } public int getHeight() { return h; } public Z getState() { return s; } @Override public void setExtraState(String key, Z value) { if (es == null) { es = new HashMap<>(); } es.put(key, value); } @Override public Z getExtraState(String key) { return es != null ? es.get(key) : null; } @Override public String toString() { return "height=" + h + ", state=" + s + (es != null ? ", extraState=" + es.toString() : ""); } @Override protected StackStateImpl clone() { try { @SuppressWarnings("unchecked") StackStateImpl clone = (StackStateImpl) super.clone(); if (es != null) { clone.es = new HashMap<>(this.es); } return clone; } catch (CloneNotSupportedException e) { throw new AssertionError("Implements Cloneable, so shouldn't throw", e); } } } /** * MatsTraceStringImpl.toString(). */ @Override public String toString() { StringBuilder buf = new StringBuilder(); CallImpl currentCall = getCurrentCall(); if (currentCall == null) { return "MatsTrace w/o CurrentCall. TraceId:" + tid + ", FlowId:" + id + "."; } String callType = currentCall.getCallType().toString(); callType = callType + spaces(8 - callType.length()); // === HEADER === buf.append("MatsTrace").append('\n') .append(" Call Flow / Initiation:").append('\n') .append(" Timestamp _________ : ").append(Instant.ofEpochMilli( getInitializedTimestamp()).atZone(ZoneId.systemDefault()).toString()).append('\n') .append(" TraceId ___________ : ").append(getTraceId()).append('\n') .append(" FlowId ____________ : ").append(getFlowId()).append('\n') .append(" Initializing App __ : ").append(getInitializingAppName()).append(",v.").append( getInitializingAppVersion()).append('\n') .append(" Initiator (from)___ : ").append(getInitiatorId()).append('\n') .append(" Init debug info ___ : ").append(getDebugInfo() != null ? getDebugInfo() : "-not present-").append('\n') .append(" Properties:").append('\n') .append(" KeepMatsTrace ___ : ").append(getKeepTrace()).append('\n') .append(" NonPersistent ___ : ").append(isNonPersistent()).append('\n') .append(" Interactive _____ : ").append(isInteractive()).append('\n') .append(" TimeToLive ______ : ").append(((tl == null) || (tl == 0)) ? "forever" : tl.toString()) .append('\n') .append(" NoAudit _________ : ").append(isNoAudit()).append('\n') .append('\n'); // === CURRENT CALL === buf.append(" Current Call: ").append(currentCall.getCallType().toString()).append('\n') .append(" Timestamp _________ : ").append(Instant.ofEpochMilli( getCurrentCall().getCalledTimestamp()).atZone(ZoneId.systemDefault())).append('\n') .append(" MatsMessageId _____ : ").append(getCurrentCall().getMatsMessageId()).append('\n') .append(" From App __________ : ").append(getCurrentCall().getCallingAppName()).append(",v.").append( getCurrentCall().getCallingAppVersion()).append('\n') .append(" From ______________ : ").append(getCurrentCall().getFrom()).append('\n') .append(" To (this) _________ : ").append(getCurrentCall().getTo()).append('\n') .append(" Call debug info ___ : ").append(currentCall.getDebugInfo() != null ? currentCall.getDebugInfo() : "-not present-").append('\n') .append(" Flow call# ________ : ").append(getCallNumber()).append('\n') .append(" Incoming State ____ : ").append(getCurrentState().map(StackState::getState) .map(Object::toString).orElse("-null-")) .append('\n') .append(" Incoming Msg ______ : ").append(currentCall.getData()).append('\n') .append(" Current SpanId ____ : ").append(Long.toString(getCurrentSpanId(), 36)).append('\n') .append(" ReplyFrom SpanId __ : ").append(currentCall.getCallType() == CallType.REPLY ? Long.toString(currentCall.getReplyFromSpanId(), 36) : "n/a (not REPLY)").append('\n'); buf.append('\n'); // === CALLS === if (getKeepTrace() == KeepMatsTrace.MINIMAL) { // MINIMAL buf.append(" initiator: (MINIMAL, so only have initiator and current call)\n"); buf.append(" "); if (an != null) { buf.append(" @").append(an); } if (av != null) { buf.append('[').append(av).append(']'); } if (h != null) { buf.append(" @").append(h); } if (ts != 0) { buf.append(" @"); DateTimeFormatter.ISO_OFFSET_DATE_TIME.formatTo( ZonedDateTime.ofInstant(Instant.ofEpochMilli(ts), TimeZone.getDefault().toZoneId()), buf); } if (iid != null) { buf.append(" #initiatorId:").append(iid); } buf.append('\n'); buf.append(" current call: (stack height: ").append(currentCall.getReplyStackHeight()).append(")\n"); buf.append(" ") .append(((CallImpl) getCallFlow().get(0)).toStringFromMatsTrace(ts, 0, 0, false)); buf.append('\n'); } else { // FULL or COMPACT // --- Initiator "Call" --- buf.append(" call#: call type\n"); buf.append(" 0 --- [Initiator]"); if (an != null) { buf.append(" @").append(an); } if (av != null) { buf.append('[').append(av).append(']'); } if (h != null) { buf.append(" @").append(h); } if (ts != 0) { buf.append(" @"); DateTimeFormatter.ISO_OFFSET_DATE_TIME.formatTo( ZonedDateTime.ofInstant(Instant.ofEpochMilli(ts), TimeZone.getDefault().toZoneId()), buf); } if (iid != null) { buf.append(" #initiatorId:").append(iid); } buf.append('\n'); int maxStackSize = c.stream().mapToInt(CallImpl::getReplyStackHeight).max().orElse(0); int maxToStageIdLength = c.stream() .mapToInt(c -> c.getTo().toString().length()) .max().orElse(0); // --- Actual Calls List> callFlow = getCallFlow(); for (int i = 0; i < callFlow.size(); i++) { boolean printNullData = (kt == KeepMatsTrace.FULL) || (i == (callFlow.size() - 1)); CallImpl call = (CallImpl) callFlow.get(i); buf.append(String.format(" %2d %s\n", i + 1, call.toStringFromMatsTrace(ts, maxStackSize, maxToStageIdLength, printNullData))); } } buf.append('\n'); // === STATES === buf.append(" ").append("states: (").append(getKeepTrace()).append(", thus ") .append(getKeepTrace() == KeepMatsTrace.FULL ? "state flow" : "state stack") .append(" - includes state (if any) for this frame, and for all reply frames below us)") .append("\n"); List> stateFlow = getStateFlow(); if (stateFlow.isEmpty()) { buf.append(" \n"); } for (int i = 0; i < stateFlow.size(); i++) { buf.append(String.format(" %2d %s", i, stateFlow.get(i))).append('\n'); } buf.append('\n'); // === REPLY TO STACK === buf.append(" current ReplyTo stack (frames below us): \n"); List stack = currentCall.getReplyStack(); if (stack.isEmpty()) { buf.append(" \n"); } else { List> stateStack = getStateStack(); for (int i = 0; i < stack.size(); i++) { buf.append(String.format(" %2d %s", i, stack.get(i).toString())) .append(" #state:").append(stateStack.get(i).getState()) .append('\n'); } } buf.append('\n'); // === SPAN ID STACK === buf.append(" current SpanId stack: \n"); List spanIdStack = getSpanIdStack(); for (int i = 0; i < spanIdStack.size(); i++) { buf.append(String.format(" %2d %s", i, Long.toString(spanIdStack.get(i), 36))); if (i == spanIdStack.size() - 1) { buf.append(" (SpanId which current ").append(currentCall.getCallType()) .append(" call is processing within)"); } if (i == 0) { buf.append(" (Root SpanId for initiator/terminator level)"); } buf.append('\n'); } return buf.toString(); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy