io.mats3.MatsInitiator Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of mats-api Show documentation
Show all versions of mats-api Show documentation
API for Mats^3: Message-based Asynchronous Transactional Staged Stateless Services.
package io.mats3;
import java.io.Closeable;
import java.util.Optional;
import java.util.function.Function;
import io.mats3.MatsEndpoint.DetachedProcessContext;
import io.mats3.MatsEndpoint.ProcessContext;
import io.mats3.MatsEndpoint.ProcessLambda;
import io.mats3.MatsEndpoint.ProcessTerminatorLambda;
import io.mats3.MatsFactory.ContextLocal;
import io.mats3.MatsFactory.FactoryConfig;
import io.mats3.MatsFactory.MatsWrapper;
/**
* Provides the means to get hold of a {@link MatsInitiate} instance for initiating Mats message flows: You fetch an
* instance implementing this interface using typically {@link MatsFactory#getDefaultInitiator()}, and then invoke
* {@link #initiate(InitiateLambda)}, where the lambda will provide you with the necessary {@link MatsInitiate} instance
* on which you have methods to construct and dispatch e.g. "send" and "request" Messages.
*
* Notice: This class is Thread Safe - you are not supposed to make one instance per message initiation, but
* rather make one (or a few) for the entire application, and use it/them for all your initiation needs. The mentioned
* {@link MatsFactory#getDefaultInitiator()} is what you typically want to use, get the instance, and keep it to perform
* all your Mats initiations. For example, in a Spring-based service, you'd typically put it in the Spring context, and
* inject it where ever there is a need to perform Mats initiations.
*
* Implementation Note: It shall be possible to use instances of MatsInitiator
as keys in a
* HashMap
, i.e. their equals and hashCode should remain stable throughout the life of the MatsFactory -
* and similar instances but with different MatsFactory are not equals. Depending on the implementation, instance
* equality may be sufficient.
*
* @author Endre Stølsvik - 2015 - http://endre.stolsvik.com
*/
public interface MatsInitiator extends Closeable {
/**
* @return the name of this MatsInitiator
. The {@link MatsFactory#getDefaultInitiator() default
* initiator}'s name is 'default'.
*/
String getName();
/**
* @return the parent {@link MatsFactory}.
*/
MatsFactory getParentFactory();
/**
* Initiates a new message ("request" or "send") out to an endpoint: You provide a lambda which is supplied the
* {@link MatsInitiate} instance on which you invoke methods to construct and dispatch messages. The
* {@link InitiateLambda#initiate(MatsInitiate)} will be invoked in a transactional context, which will also include
* database operations that are invoked inside the lambda if the transaction manager for the MatsFactory is also
* used for database operations. This also implies that either all messages produced in the lambda will be sent, or
* none will.
*
* @param lambda
* provides the {@link MatsInitiate} instance on which to create the message to be sent.
* @throws MatsBackendException
* if the Mats implementation cannot connect to the underlying message broker, or are having problems
* interacting with it.
* @throws MatsMessageSendException
* if the Mats implementation cannot send the messages after it has executed the initiation lambda and
* committed external resources - please read the JavaDoc of that class.
*/
void initiate(InitiateLambda lambda) throws MatsBackendException, MatsMessageSendException;
/**
* Variant of {@link #initiate(InitiateLambda)} where the two error conditions are raised as unchecked exceptions
* (But please understand the implications of {@link MatsMessageSendRuntimeException}!)
*
* @param lambda
* provides the {@link MatsInitiate} instance on which to create the message to be sent.
* @throws MatsBackendRuntimeException
* if the Mats implementation cannot connect to the underlying message broker, or are having problems
* interacting with it.
* @throws MatsMessageSendRuntimeException
* if the Mats implementation cannot send the messages after it has executed the initiation lambda and
* committed external resources - please read the JavaDoc of {@link MatsMessageSendException}.
*/
void initiateUnchecked(InitiateLambda lambda) throws MatsBackendRuntimeException,
MatsMessageSendRuntimeException;
/**
* Will be thrown by the {@link MatsInitiator#initiate(InitiateLambda)}-method if it is not possible at this time to
* establish a connection to the underlying messaging system (e.g. to ActiveMQ if used in JMS implementation with
* ActiveMQ as JMS Message Broker), or if there was any other kind of problems interacting with the MQ or other
* external resources (typically database) in the same transactional demarcation. Do note that in all cases of this
* exception, any other external resource (again, typically database) will not have been committed -
* unlike if you get the {@link MatsMessageSendException}.
*/
class MatsBackendException extends Exception {
public MatsBackendException(String message) {
super(message);
}
public MatsBackendException(String message, Throwable cause) {
super(message, cause);
}
}
/**
* Will be thrown by the {@link MatsInitiator#initiate(InitiateLambda)}-method if Mats fails to send the messages
* after the {@link InitiateLambda} has been run, any external resource (typically DB) has been committed, and
* then some situation occurs that makes it impossible to send out messages. (Some developers might recognize
* this as the "VERY BAD!-initiation" situation).
*
* This is a rare, but unfortunate situation, but which is hard to guard completely against, in particular in the
* "Best Effort 1-Phase Commit" paradigm that the current Mats implementations runs on. What it means, is that if
* you e.g. in the initiate-lambda did some "job allocation" logic on a table in a database, and based on that
* allocation sent out e.g. 5 messages, the job allocation will now have happened, but the messages have
* not actually been sent. The result is that in the database, you will see those jobs as processed
* (semantically "started processing"), but in reality the downstream endpoints never started working on them since
* the message was not actually sent out.
*
* This situation can to a degree be alleviated if you catch this exception, and then use a compensating
* transaction to de-allocate the jobs in the database again. However, since bad things often happen in
* clusters, you might not be able to do the de-allocation either (due to the database becoming inaccessible at the
* same instant - e.g. the reason that the messages could not be set was that the network cable became unplugged, or
* that this node actually lost power at that instant). A way to at least catch when this happens, is to employ a
* state machine to the job allocation logic: First pick jobs for this node by setting the state column of
* job-entries whose state is "UNPROCESSED" to some status like "ALLOCATED" (along with a column of which node
* allocated them (i.e. "hostname" of this node) and a column for timestamp of when they were allocated). In the
* initiator, you pick the jobs that was allocated to this node, set the status to "SENT" and send the outgoing
* messages. Finally, in the terminator endpoint (which you specify in the initiation), you set the status to
* "DONE". Then you make a health check: Assuming that in normal conditions such jobs should always be processed in
* seconds, you make a health check that scans the table for rows which have been in the "ALLOCATED" or "SENT"
* status for e.g. 15 minutes: Such rows are very suspicious, and should be checked up by humans. Sitting in
* "ALLOCATED" status would imply that the node that allocated the job went down (and has not (yet) come back up)
* before it managed to initiate the messages, while sitting in "SENT" would imply that the message had started, but
* not gotten through the processing: Either that message flow sits in a downstream Dead Letter Queue due to some
* error, or you ended up in the situation explained here: The database commit went through, but the messages was
* not sent.
*
* Please note that this should, in a somewhat stable operations environment, happen extremely seldom: What needs to
* occur for this to happen, is that in the sliver of time between the commit of the database and the commit of the
* message broker, this node crashes, the network is lost, or the message broker goes down. Given that a check for
* broker liveliness is performed right before the database commit, that time span is very tight. But to make the
* most robust systems that can monitor themselves, you should consider employing a state machine handling as
* outlined above. You might never see that health check trip, but now you can at least sleep without thinking about
* that 1 billion dollar order that was never processed.
*
* PS: Best effort 1PC: Two transactions are opened: one for the message broker, and one for the database. The
* business logic and possibly database reads and changes are performed. The database is committed first, as that
* has many more failure scenarios than the message systems, e.g. data or code problems giving integrity constraint
* violations, and spurious stuff like MS SQL's deadlock victim, etc. Then the message queue is committed, as the
* only reason for the message broker to not handle a commit is basically that you've had infrastructure problems
* like connectivity issues or that the broker has crashed.
*
* Notice that it has been decided to not let this exception extend the {@link MatsBackendException}, even though it
* is definitely a backend problem. The reason is that it in all situations where {@code MatsBackendException} is
* raised, the other resources have not been committed yet, as opposed to situations where this
* {@code MatsMessageSendException} is raised. Luckily, in this time and age, we have multi-exception catch blocks
* if you want to handle both the same.
*/
class MatsMessageSendException extends Exception {
public MatsMessageSendException(String message) {
super(message);
}
public MatsMessageSendException(String message, Throwable cause) {
super(message, cause);
}
}
/**
* Unchecked variant of the {@link MatsBackendException}, thrown from the {@link #initiateUnchecked(InitiateLambda)}
* variant of initiate().
*/
class MatsBackendRuntimeException extends RuntimeException {
public MatsBackendRuntimeException(String message) {
super(message);
}
public MatsBackendRuntimeException(String message, Throwable cause) {
super(message, cause);
}
}
/**
* Unchecked variant of the {@link MatsMessageSendException}, thrown from the
* {@link #initiateUnchecked(InitiateLambda)} variant of initiate().
*/
class MatsMessageSendRuntimeException extends RuntimeException {
public MatsMessageSendRuntimeException(String message) {
super(message);
}
public MatsMessageSendRuntimeException(String message, Throwable cause) {
super(message, cause);
}
}
/**
* {@link FunctionalInterface @FunctionalInterface} for the "initiate lambda" - what you supply to the
* {@link #initiate(InitiateLambda) initiate} method.
*/
@FunctionalInterface
interface InitiateLambda {
void initiate(MatsInitiate init);
}
/**
* Closes any underlying backend resource.
*/
@Override
void close();
/**
* An implementation of this interface is given to you when you want to initiate a new Mats Flow. It is the argument
* given to the {@link InitiateLambda#initiate(MatsInitiate) InitiateLambda.initiate(..)} lambda you implement.
*
* To initiate a message "from the outside", i.e. from synchronous application code, you must get a
* {@link MatsInitiator}, e.g. by invoking {@link MatsFactory#getDefaultInitiator()}, and then invoke
* {@link MatsInitiator#initiate(InitiateLambda) matsInitiator.initiate(..)} on that.
*
* To initiate a new message "from the inside", i.e. while already inside a {@link MatsStage processing stage} of an
* endpoint, you use the initiate method on the stage lambda's ProcessContext:
* {@link ProcessContext#initiate(InitiateLambda) processContext.initiate(..)}. (Notice that initiating a new
* Mats Flow from within a processing stage is a much less common operation than performing a
* {@link ProcessContext#request(String, Object) request}, {@link ProcessContext#reply(Object) reply}, and other
* calls on the existing Mats Flow. Initiating a new message from within a processing stage is effectively a fork of
* the Mats Flow you are in.)
*
* @author Endre Stølsvik - 2015-07-11 - http://endre.stolsvik.com
*/
interface MatsInitiate {
/**
* Sets the supplied Trace Id (or appends with a joining "|" in case of
* {@link ProcessContext#initiate(InitiateLambda) initiation within a stage}, unless the first char is a "!"),
* which is solely used for logging and debugging purposes. It should be unique, at least to a degree where it
* is very unlikely that you will have two identical traceIds within a couple of years ("guaranteed
* globally unique through all time" is not relevant).
*
* If the first character is a "!", it will be removed - in the case that
* {@link ProcessContext#initiate(InitiateLambda) initiation within a stage}, this denotes that the TraceId
* should not be prepended with the stage's TraceId.
*
* Since this is very important when doing distributed and asynchronous architectures, it is mandatory.
*
* The traceId follows a Mats processing from the initiation until it is finished, usually in a Terminator.
*
* It should be a world-unique Id, preferably set all the way back when some actual person performed some event.
* E.g. in a "new order" situation, the Id would best be set when the user clicked the "place order" button on
* the web page - or maybe even derived from the event when he first initiated the shopping cart - or maybe even
* when he started the session. The point is that when using e.g. Kibana or Splunk to track events that led to
* some outcome, a robust, versatile and information-rich track/trace Id makes wonders.
*
* It is strongly recommended to use small, dense, information rich Trace Ids. Sticking in an UUID
* as Trace Id certainly fulfils the uniqueness-requirement, but it is a crappy solution, as it by itself does
* not give any hint of source, cause, relevant entities, or goal. (It isn't even dense for the uniqueness an
* UUID gives, which also is way above the required uniqueness unless you handle billions of such messages per
* minute. A random alphanum (A-Z,a-z,0-9) string of much smaller length would give plenty enough
* uniqueness). The following would be a much better Trace Id than a random UUID, which follows some scheme
* that could be system wide: "Web.placeOrder[cid:43512][cart:xa4ru5285fej]qz7apy9". From this example TraceId
* we could infer that it originated at the Web system, it regards Placing an order for
* Customer Id 43512, it regards the Shopping Cart Id xa4ru5285fej, and it contains some
* uniqueness ('qz7apy9') generated at the initiating, so that even if the customer managed to click three times
* on the "place order" button for the same cart, you would still be able to separate the resulting three
* different Mats call flows.
*
* You should consider storing the traceId as a column in any inserted rows in any databases that was affected
* by this call flow, e.g. in the row for a placed order, you'd have the traceId as a column. This both to be
* able to tie this db row back to the logging system, but also if new Mats flows are initiated based on this
* row, you can include the order TraceId in the new TraceId (with either "|" or "+" as separator).
*
* For situations where one Mats Flow "forks" new Mats flows, make new TraceIds for the "sub flows" by tying
* together the existing TraceId and a new Sub TraceId with a "|". In the order/shipping example, the user would
* first have placed an order, resulting in an entry in an order database table - where the traceId was stored
* along. Then, when some batch process Mats Flow initiated the filling process, you would pick up the order's
* traceId and append that to the sub Mats Flow. Sub Flow Mats TraceId = "{Filling Batch Id}|{Existing Order
* TraceId}". If you took this all the way, you could in your logging system follow the "total flow" from the
* initial click on the web page, through the order book, order processing, shipment, and all the way to the
* delivered and signed package on the person's door - even if the entirety of the order fulfillment consists of
* multiple "stop and go" sub-flows (the stops being where the process stays for a while as an entry in a
* database, here "order", then possibly "filling", "shipping" and finally "delivery", or whatever your multiple
* processes' flows consists of).
*
* The "+" character should be used to indicate that an "initiated from the outside" TraceId is concatenated by
* multiple parts, which so far has been used to pick up an X-Request-ID header from a HTTP-request, and
* prepending this to the TraceId for a resulting Mats-flow, i.e. Mats TraceId="{X-Request-ID}+{Flow TraceID}".
* (Check out {@link FactoryConfig#setInitiateTraceIdModifier(Function)} for a way to automate this.)
*
* The difference between "|" and "+" is meant to be that "|" indicates that there is a Mats Flow TraceId on
* both sides of the "|", while the "+" is used when a "from the outside"-initiated TraceId is built up by
* multiple pieces, e.g. X-Request-Id, or Batch Run Number, on the left side. Remember, this is all meant for
* human consumption, in particular when debugging. Do what you feel is meaningful - the Mats system doesn't
* care!
*
* (For the default implementation "JMS Mats", the Trace Id is set on the MDC
of the SLF4J logging
* system, using the key "traceId". Since this implementation logs a few lines per handled message, in addition
* to any log lines you emit yourself, you will, by collecting the log lines in a common log system (e.g. the
* ELK stack), be able to very easily follow the processing trace through all the services the call flow
* passes.)
*
* @param traceId
* the traceId that will follow the mats trace from initiation (first call) to termination (last
* call, where the flow ends). Although the type is CharSequence
, the implementation
* will immediately do toString()
on the instance to fix the value.
* @return the {@link MatsInitiate} for chaining.
*/
MatsInitiate traceId(CharSequence traceId);
/**
* Hint to the underlying implementation to which level of call and state history the underlying protocol should
* retain. The default (unless configured otherwise), {@link KeepTrace#COMPACT}, keeps a trail of all the stages
* the flow has been through, but drops state and DTOs that aren't relevant for the current message. You might
* want to use {@link KeepTrace#FULL} while developing new functionality, while once a certain call flow
* stabilizes, without much errors or DLQs, you should consider initiating it with {@link KeepTrace#COMPACT}
* or even {@link KeepTrace#MINIMAL} (the latter also dropping the "trail", thus only keeping information
* strictly relevant for the current message).
*
* This functionality is solely for debugging, the Mats endpoints works identically with any setting,
* thus it is a tradeoff between performance (size of messages) and debuggability. The resulting kept trace
* would typically be visible in a "toString()" of the {@link ProcessContext} - or in an external (e.g.
* Brokerside) debugging/tracing system.
*
* @return the {@link MatsInitiate} for chaining.
*/
MatsInitiate keepTrace(KeepTrace keepTrace);
/**
* Enable unreliable, but fast, messaging! Hint to the underlying implementation that it does not matter
* that much if this message is lost. The implication is that the messages that this flow consist of are
* unreliable - typically, if the MQ broker is restarted, any outstanding "non persistent" messages are lost.
* (Also, some backends will (in default config) lose the Dead Letter Queue (DLQ) functionality when this is
* used, where a ordinary persistent message would be DLQed if it failed to be delivered to an endpoint. This
* can severely impact monitoring (as you don't get a build-up of DLQs when things goes wrong - only log lines)
* and to a degree debugging (since you don't have the DLQ'ed messages to look at).)
*
* This is only usable for "pure GET"-style requests without any state changes along the flow,
* i.e. "AccountService.getBalances", for display to an end user. If such a message is lost, the world won't
* go under.
*
* The upshot here is that non-persistent messaging typically is blazingly fast and is way less resource
* demanding, as the messages will not have to (transactionally) be stored in non-volatile storage. It is
* therefore wise to actually employ this feature where it makes sense (which, again, is only relevant
* for side-effect free GET-style requests).
*
* @return the {@link MatsInitiate} for chaining.
*/
MatsInitiate nonPersistent();
/**
* Same as {@link #nonPersistent()}, but you can set a time-to-live too. If the message gets this old and have
* not yet been delivered to the receiving Mats endpoint, it will be deleted and never delivered.
*
* This functionality often makes sense for messages that are both {@link #interactive() interactive} and
* {@link #nonPersistent() non-persistent}: Such messages shall only be "getters" free of any side effects (i.e.
* no state is changed by the entire message flow), and where a human is actively waiting for the reply. If
* there is a situation where such messages aren't consumed due to the receiving service having problems, it
* does not make sense to use processing resources to handle a massive stack of these messages when the
* consumption is restored an hour later, as e.g. the synchronously waiting HTTP call that was waiting for the
* reply has timed out, and the waiting human is probably long gone anyway.
*
* Notice on use: This should NOT be employed for message flows where any stage might change any
* state, i.e. message flows with side effects (Think "PUT", "POST" and "DELETE"-style messages) - which also
* should NOT employ {@link #nonPersistent() non-persistent} messaging. The rationale is that such
* messages should never just cease to exists, for any reason - they should have both guaranteed delivery and
* execution. You should also never use this to handle any business logic, e.g. some kind of order time-out
* where an order is only valid until 21:00, or something like this. This both because of the "Note on
* implementation" below, and that the entire facility of "time to live" is optional both for Mats and for
* the underlying message queue system.
*
* Notice on implementation: If the message is a part of a multi-message flow, which most Mats
* initiations pretty much invariably is (a request consists of a request-message and a reply-message), this TTL
* will be set afresh on every new message in the flow, possibly with the amount of time taken in the processing
* of the stage deducted. However, the time that the message waited in queue will not be deducted. The
* effective TTL of the flow might therefore be a multiple of what is set here. An example: The TTL of an
* initiation is set to 5000 ms. The request message stays 4 seconds in queue, before being received and
* processed, where the processing took 100 ms. The reply-message will thus have its TTL set to 4900 ms: 5000 ms
* TTL - 100 ms for processing. The reply message stays 4 seconds in queue before being received. The total
* "time in flight" has now been 8.1 seconds, and there was still 900 ms left of the reply-message's TTL. The
* rationale for not deducting queue-time on the subsequent message is that there is no easy way to get the
* "queue time" which does not involve taking the difference between two timestamps, but in a multi-server
* architecture there is a clear possibility of clock skews between different services, even for instances of
* the same service. You could then deduce a too high queue time, deducting a too high value from the
* reply-message's TTL, and effectively time out the full message flow too early. However, for the intended use
* case - to hinder build-up of messages that will nevertheless be valueless when the answer is received since
* the interactively waiting human is long gone - this is no big problem.
*
* @param timeToLiveMillis
* the number of milliseconds before this message is timed out and thus will never be delivered - 0
* means "live forever", and this is the default.
* @return the {@link MatsInitiate} for chaining.
*/
MatsInitiate nonPersistent(long timeToLiveMillis);
/**
* Prioritize this Mats flow! Hint to the underlying implementation that a human is actually waiting for
* the result of a request, and that the flow therefore should be prioritized. This status will be kept through
* the entire flow, so that all messages in the flow are prioritized. This makes it possible to use the same
* "AccountService.getBalances" service both for the Web Application that the user facing GUI are employing, and
* the batch processing of a ton of orders. Without such a feature, the interactive usage could be backlogged by
* the batch process, while if the interactive flag is set, it will bypass the backlog of "ordinary" messages.
*
* This implies that MATS defines two levels of prioritization: "Ordinary" and "Interactive". Most processing
* should employ the default, i.e. "Ordinary", while places where a human is actually waiting for the
* reply should employ the fast-lane, i.e. "Interactive". It is imperative to not abuse this
* feature, or else it will loose its value: If batches are going too slow, nothing will be gained by setting
* the interactive flag except destroying the entire point of this feature. Instead use higher parallelism: By
* increasing {@link MatsConfig#setConcurrency(int) concurrency} or the number of nodes that is running the
* problematic endpoint or stage; increase the speed and/or throughput of external systems like the database; or
* somehow just code the whole thing to be faster!
*
* It will often make sense to set both this flag, and the {@link #nonPersistent()}, at the same time. E.g. when
* you need to show the account balance for a customer: It both needs to skip past any bulk/batch queue of such
* requests (since a human is literally waiting for the result), but it is also a "pure GET"-style request, not
* altering state whatsoever, so it can also be set to non-persistent.
*
* @return the {@link MatsInitiate} for chaining.
*/
MatsInitiate interactive();
/**
* Marks this Mats flow as not relevant for auditing.
*
* When considering auditing ("event sourcing"-style) of all messages, one quickly realizes that there are very
* many messages that aren't that interesting to log. These are pure getters employed to show information to
* users, and even worse in this respect, "Are you up?"-type health checks.
*
* This flag is here to mark messages as such: "You will gain no historic insight in logging the following
* message flow!". This flag should NOT be set for ANY messages that (can potentially) change state in any
* part of the total system (i.e. "permanent state", e.g. a row in a database). More subtle, the flag should
* also not be set for "getters" that are performed to decide upon a state - e.g. for validation of new orders:
* The getter that checks credit should be audited. If such a getter is a part of a Mats flow, this should not
* be a problem, as the initiator of the "add order" Mats flow would obviously not set noAudit(). However, if it
* is coded as multiple "stop and go" flows, i.e. add the order to some incoming order table. Then a next,
* separate Mats flow is validation: That getter should be audited, hence do not set noAudit().
*
* Note: It might be interesting to log that such messages actually happened, but not the content of them. This
* is to be able to tally them, i.e. "WebService.healthCheck is invoking AccountService.webStatus 52389052
* times per day" - both to see that it is probably a bit excessive, and to see that there is traffic there
* at all (since AccountService.webStatus seems to be a pretty specific endpoint). However, there is probably
* not much use in storing the contents of such calls - and maybe not retain the calls for more than a
* few months.
*
* @return the {@link MatsInitiate} for chaining.
*/
MatsInitiate noAudit();
/**
* Sets the originating/initiating "synthetic endpoint Id" - only used for statistics/tracing/debugging. If this
* message is initiated from within a stage, i.e. by use of
* {@link ProcessContext#initiate(InitiateLambda)}, the 'from' property is already set to the stageId of the
* currently processing Stage, but it can be overridden if desired. It is important that you do not make
* this into a dynamic string, i.e. do not add some Id to it (such Ids should go into the traceId).
*
* It it is smart to decide on a common prefix for all Mats Endpoints and InitiatorIds for a particular service.
* E.g. "OrderService" or "InventoryService" or something like this.
*
* A good value that would be of use when debugging a call trace is something following a structure like
* "OrderService.REST.place_order_from_user"
, this example trying to convey that it is from the
* OrderSystem, coming in over its REST endpoint "place_order_from_user". Note that there are no e.g. userId
* there.
*
* NOTE: This is only used for tracing/debugging (in particular, it is not related to the
* {@link #replyTo(String, Object) replyTo} functionality), but should be set to something that will give
* insights when you try to make sense of call flows. Think of a introspection system showing a histogram of
* where messages are initiated, so that you can see that 45% of the messages are coming from the OrderSystem's
* REST endpoints, and 15% of all initiations are its "place_order_from_user". This also implies that it
* shall not be a dynamic value, i.e. do not put something that will vary between each call, that is, do NOT add
* the user's Id or something like that. Such dynamic elements is what the {@link #traceId(CharSequence)} is
* for.
*
* @param initiatorId
* the originating/initiating "synthetic endpoint Id" - only used for tracing/debugging.
* @return the {@link MatsInitiate} for chaining.
*/
MatsInitiate from(String initiatorId);
/**
* Sets which MATS Endpoint this message should go.
*
* @param endpointId
* to which MATS Endpoint this message should go.
* @return the {@link MatsInitiate} for chaining.
*/
MatsInitiate to(String endpointId);
/**
* Specified which MATS Endpoint the reply of the invoked Endpoint should go to.
*
* @param endpointId
* which MATS Endpoint the reply of the invoked Endpoint should go to.
* @param replySto
* the object that should be provided as STO to the service which get the reply.
* @return the {@link MatsInitiate} for chaining.
*/
MatsInitiate replyTo(String endpointId, Object replySto);
/**
* A "pub-sub" variant of {@link #replyTo(String, Object) replyTo}, where the reply will go to the specified
* endpointId which must be a
* {@link MatsFactory#subscriptionTerminator(String, Class, Class, ProcessTerminatorLambda)
* SubscriptionTerminator}.
*
* @param endpointId
* which MATS Endpoint the reply of the invoked Endpoint should go to.
* @param replySto
* the object that should be provided as STO to the service which get the reply.
* @return the {@link MatsInitiate} for chaining.
*/
MatsInitiate replyToSubscription(String endpointId, Object replySto);
/**
* Adds a property that will "stick" with the call flow from this call on out. Read more on
* {@link ProcessContext#setTraceProperty(String, Object)}.
*
* @param propertyName
* the name of the property
* @param propertyValue
* the value of the property, which will be serialized using the active MATS serializer.
* @see ProcessContext#setTraceProperty(String, Object)
* @see ProcessContext#getTraceProperty(String, Class)
*/
MatsInitiate setTraceProperty(String propertyName, Object propertyValue);
/**
* Adds a binary payload to the outgoing request message, e.g. a PDF document.
*
* The rationale for having this is to not have to encode a largish byte array inside the JSON structure that
* carries the Request DTO - byte arrays represent very badly in JSON.
*
* Note: The byte array is not compressed (as might happen with the DTO), so if the payload is large, you might
* want to consider compressing it before attaching it (and will then have to decompress it on the receiving
* side).
*
* @param key
* the key on which to store the byte array payload. The receiver will have to use this key to get
* the payload out again, so either it will be a specific key that the sender and receiver agree
* upon, or you could generate a random key, and reference this key as a field in the Request DTO.
* @param payload
* the byte array.
* @return the {@link MatsInitiate} for chaining.
* @see #addString(String, String)
*/
MatsInitiate addBytes(String key, byte[] payload);
/**
* Adds a String payload to the outgoing request message, e.g. a XML, JSON or CSV document.
*
* The rationale for having this is to not have to encode a largish string document inside the JSON structure
* that carries the Request DTO.
*
* Note: The String payload is not compressed (as might happen with the DTO), so if the payload is large, you
* might want to consider compressing it before attaching it and instead use the
* {@link #addBytes(String, byte[]) addBytes(..)} method (and will then have to decompress it on the receiving
* side).
*
* @param key
* the key on which to store the String payload. The receiver will have to use this key to get the
* payload out again, so either it will be a specific key that the sender and receiver agree upon, or
* you could generate a random key, and reference this key as a field in the Request DTO.
* @param payload
* the string.
* @return the {@link MatsInitiate} for chaining.
* @see #addBytes(String, byte[])
*/
MatsInitiate addString(String key, String payload);
/**
* Adds a measurement of a described variable, in a base unit, for this Initiation - be sure to understand
* that the three String parameters are constants for each measurement. To exemplify, you may measure
* five different things in an Initiation, i.e. "number of items in order", "total amount for order in dollar",
* etc - and each of these obviously have different metricId, metricDescription and possibly different baseUnit
* from each other. BUT, the specific arguments for "number of items in order" (outside the measure itself!)
* shall not change between one Initiation and the next (of the same "type", i.e. place in the code): e.g. the
* metricDescription shall NOT be dynamically constructed to e.g. say "Number of items in order 1234
* for customer 5678".
*
* Note: It is illegal to use the same 'metricId' for more than one measurement for a given Initiation, and this
* also goes between measurements and {@link #logTimingMeasurement(String, String, long, String...) timing
* measurements}.
*
* Inclusion as metric by plugin 'mats-intercept-micrometer': A new meter will be created (and cached),
* of type DistributionSummary
, with the 'name' set to
* "mats.exec.ops.measure.{metricId}.{baseUnit}"
("measure"->"time" for timings), and
* 'description' to description. (Tags/labels already added on the meter by the plugin include 'appName',
* 'initiatorId', 'initiatingAppName', 'stageId' (for stages), and 'initiatorName' (for inits)). Read about
* parameter 'labelKeyValue' below.
*
* Inclusion as log line by plugin 'mats-intercept-logging': A log line will be output by each added
* measurement, where the MDC for that log line will have an entry with key
* "mats.ops.measure.{metricId}.{baseUnit}"
. Read about parameter 'labelKeyValue' below.
*
* It generally makes most sense if the same metrics are added for each processing of a particular Initiation,
* i.e. if the "number of items" are 0, then that should also be recorded along with the "total amount for order
* in dollar" as 0, not just elided. Otherwise, your metrics will be skewed.
*
* You should use a dot-notation for the metricId if you want to add multiple meters with a
* hierarchical/subdivision layout.
*
* The vararg 'labelKeyValue' is an optional element where the String-array consist of one or several alternate
* key, value pairs. Do not employ this feature unless you know what the effects are, and you actually need
* it! This will be added as labels/tags to the metric, and added to the SLF4J MDC for the measurement log
* line with the key being "mats.ops.measure.{metricId}.{labelKey}"
("measure"->"time" for
* timings). The keys should be constants as explained for the other parameters, while the value can change, but
* only between a given set of values (think enum
) - using e.g. the 'customerId' as value doesn't
* make sense and will blow up your metric cardinality. Notice that if you do employ e.g. two labels, each
* having one of three values, you'll effectively create 9 different meters, where your measurement will
* go to one of them.
*
* NOTICE: If you want to do a timing, then instead use
* {@link #logTimingMeasurement(String, String, long, String...)}
*
* @param metricId
* constant, short, possibly dot-separated if hierarchical, id for this particular metric, e.g.
* "items" or "amount", or "db.query.orders".
* @param metricDescription
* constant, textual description for this metric, e.g. "Number of items in customer order", "Total
* amount of customer order"
* @param baseUnit
* the unit for this measurement, e.g. "quantity" (for a count measure), "dollar" (for an amount), or
* "bytes" (for a document size).
* @param measure
* value of the measurement
* @param labelKeyValue
* a String-vararg array consisting of alternate key,value pairs which will becomes labels or tags or
* entries for the metrics and log lines. Read the JavaDoc above; the keys shall be "static" for a
* specific measure, while the values can change between a specific small set values.
* @return the {@link MatsInitiate} for chaining.
*/
MatsInitiate logMeasurement(String metricId, String metricDescription, String baseUnit, double measure,
String... labelKeyValue);
/**
* Same as {@link #logMeasurement(String, String, String, double, String...) addMeasurement(..)}, but
* specifically for timings - Read that JavaDoc!
*
* Note: It is illegal to use the same 'metricId' for more than one measurement for a given Initiation, and this
* also goes between timing measurements and {@link #logMeasurement(String, String, String, double, String...)
* measurements}.
*
* For the metrics-plugin 'mats-intercept-micrometer' plugin, the 'baseUnit' argument is deduced to whatever is
* appropriate for the receiving metrics system, e.g. for Prometheus it is "seconds", even though you always
* record the measurement in nanoseconds using this method.
*
* For the logging-plugin 'mats-intercept-logging' plugin, the timing in the log line will be in milliseconds
* (with fractions), even though you always record the measurement in nanoseconds using this method.
*
* @param metricId
* constant, short, possibly dot-separated if hierarchical, id for this particular metric, e.g.
* "db.query.orders" or "calcprofit".
* @param metricDescription
* constant, textual description for this metric, e.g. "Time taken to execute order query", "Time
* taken to calculate profit or loss".
* @param nanos
* time taken in nanoseconds
* @param labelKeyValue
* a String-vararg array consisting of alternate key,value pairs which will becomes labels or tags or
* entries for the metrics and log lines. Read the JavaDoc at
* {@link #logMeasurement(String, String, String, double, String...) addMeasurement(..)}
* @return the {@link MatsInitiate} for chaining.
*/
MatsInitiate logTimingMeasurement(String metricId, String metricDescription, long nanos,
String... labelKeyValue);
/**
* The standard request initiation method: All of from, to and replyTo must be set. A message is sent to
* a service, and the reply from that service will come to the specified reply endpointId, typically a
* terminator.
*
* @param requestDto
* the object which the endpoint will get as its incoming DTO (Data Transfer Object).
*/
MessageReference request(Object requestDto);
/**
* Variation of the request initiation method, where the incoming state is sent along.
*
* This only makes sense if the same code base "owns" both the initiation code and the endpoint to which this
* message is sent. It is mostly here for completeness, since it is possible to send state along with
* the message, but if employed between different services, it violates the premise that MATS is built on: State
* is private to the stages of a multi-stage endpoint, and the Request and Reply DTOs are the public interface.
*
* @param requestDto
* the object which the target endpoint will get as its incoming DTO (Data Transfer Object).
* @param initialTargetSto
* the object which the target endpoint will get as its STO (State Transfer Object).
*/
MessageReference request(Object requestDto, Object initialTargetSto);
/**
* Sends a message to an endpoint, without expecting any reply ("fire-and-forget"). The 'reply' parameter must
* not be set.
*
* Note that the difference between request(..)
and send(..)
is only that replyTo is
* not set for send, otherwise the mechanism is exactly the same.
*
* @param messageDto
* the object which the target endpoint will get as its incoming DTO (Data Transfer Object).
*/
MessageReference send(Object messageDto);
/**
* Variation of the {@link #send(Object)} method, where the incoming state is sent along.
*
* This only makes sense if the same code base "owns" both the initiation code and the endpoint to which this
* message is sent. It is mostly here for completeness, since it is possible to send state along with
* the message, but if employed between different services, it violates the premise that MATS is built on: State
* is private to the stages of a multi-stage endpoint, and the Request and Reply DTOs are the public interface.
*
* @param messageDto
* the object which the target endpoint will get as its incoming DTO (Data Transfer Object).
* @param initialTargetSto
* the object which the target endpoint will get as its STO (State Transfer Object).
*/
MessageReference send(Object messageDto, Object initialTargetSto);
/**
* Sends a message to a
* {@link MatsFactory#subscriptionTerminator(String, Class, Class, MatsEndpoint.ProcessTerminatorLambda)
* SubscriptionTerminator}, employing the publish/subscribe pattern instead of message queues (topic in JMS
* terms). This means that all of the live servers that are listening to this endpointId will receive the
* message, and if there are no live servers, then no one will receive it.
*
* The concurrency of a SubscriptionTerminator is always 1, as it only makes sense for there being only one
* receiver per server - otherwise it would just mean that all of the active listeners on one server would get
* the message, per semantics of the pub/sub.
*
* It is only possible to publish to SubscriptionTerminators as employing publish/subscribe for multi-stage
* services makes no sense.
*
* @param messageDto
* the object which the target endpoint will get as its incoming DTO (Data Transfer Object).
*/
MessageReference publish(Object messageDto);
/**
* Variation of the {@link #publish(Object)} method, where the incoming state is sent along.
*
* This only makes sense if the same code base "owns" both the initiation code and the endpoint to which this
* message is sent. The possibility to send state along with the request makes most sense with the publish
* method: A SubscriptionTerminator is often paired with a Terminator, where the Terminator receives the
* message, typically a reply from a requested service, along with the state that was sent in the initiation.
* The terminator does any "needs to be guaranteed to be performed" state changes to e.g. database, and then
* passes the incoming message - along with the same state it received - on to a SubscriptionTerminator.
* The SubscriptionTerminator performs any updates of any connected GUI clients, or for any other local states,
* e.g. invalidation of caches, on all live servers listening to that endpoint, the point being that if
* no servers are live at that moment, no one will process that message - but at the same time, there is
* obviously no GUI clients connected, nor are there are local state in form of caches that needs to be
* invalidated.
*
* @param messageDto
* the object which the target endpoint will get as its incoming DTO (Data Transfer Object).
* @param initialTargetSto
* the object which the target endpoint will get as its STO (State Transfer Object).
*/
MessageReference publish(Object messageDto, Object initialTargetSto);
/**
* Unstashes a Mats Flow that have been previously {@link ProcessContext#stash() stashed}. To be able to
* deserialize the stashed bytes to instances provided to the supplied {@link ProcessLambda}, you need to
* provide the classes of the original stage's Reply, State and Incoming objects.
*
* @param stash
* the stashed bytes which now should be unstashed
* @param replyClass
* the class which the original stage originally would reply with.
* @param stateClass
* the class which used for state in the original stage (endpoint) - or Void.TYPE
(i.e.
* void.class
) if none.
* @param incomingClass
* the class which the original stage gets as incoming DTO.
* @param lambda
* the stage lambda which should now be executed instead of the original stage lambda where stash was
* invoked.
* @param
* type of the ReplyClass
* @param
* type of the StateClass
* @param
* type of the IncomingClass
*/
void unstash(byte[] stash,
Class replyClass,
Class stateClass,
Class incomingClass,
ProcessLambda lambda);
/**
* Provides a way to get hold of (optional) attributes/objects from the Mats implementation, either specific to
* the Mats implementation in use, or configured into this instance of the Mats implementation. Mirrors the same
* method at {@link ProcessContext#getAttribute(Class, String...)}. There is also a ThreadLocal-accessible
* version at {@link ContextLocal#getAttribute(Class, String...)}.
*
* Mandatory: If the Mats implementation has a transactional SQL Connection, it shall be available by
* 'context.getAttribute(Connection.class)'
.
*
* @param type
* The expected type of the attribute
* @param name
* The (optional) (hierarchical) name(s) of the attribute.
* @param
* The type of the attribute.
* @return Optional of the attribute in question, the optionality pointing out that it depends on the Mats
* implementation or configuration whether it is available.
*
* @see ProcessContext#getAttribute(Class, String...)
* @see ContextLocal#getAttribute(Class, String...)
*/
Optional getAttribute(Class type, String... name);
/**
* @return default this
for implementations, overridden by wrappers.
*/
default MatsInitiate unwrapFully() {
return this;
}
}
/**
* Reference information about the outgoing message.
*/
interface MessageReference {
/**
* @return the globally unique Mats MessageId of the outgoing message - which will be available on the incoming
* side as {@link DetachedProcessContext#getMatsMessageId()} (where it could also be used to catch
* double deliveries, as it shall be utterly unique to this particular sent message). You could
* conceivably store this along with the order row in the database or something like this, i.e. "this is
* the Id of the particular Mats Message that sent this order on its way".
*/
String getMatsMessageId();
}
/**
* A hint to the underlying implementation of how much historic debugging information for the call flow should be
* retained in the underlying protocol.
*/
enum KeepTrace {
/**
* Default: Keep all history for request and reply DTOs, and all history for state STOs.
*
* All calls with data and state should be kept, which e.g means that at the Terminator, all request and reply
* DTOs, and all STOs (with their changing values between each stage of a multi-stage endpoint) will be present
* in the underlying protocol.
*/
FULL,
/**
* Nulls out Data for other than current call while still keeping the meta-info for the call history, and
* condenses State to a pure stack.
*
* This is a compromise between FULL and MINIMAL, where the DTOs and STOs except for the ones needed in the
* stack, are "nulled out", while the call trace itself (with metadata) is still present, which e.g. means that
* at the Terminator, you will know all endpoints and stages that the call flow traversed, but not the data or
* state except for what is pertinent for the Terminator.
*/
COMPACT,
/**
* Only keep the current call, and condenses State to a pure stack.
*
* Keep zero history, where only the current call, and only the STOs needed for the current stack, are
* present. This e.g. means that at the Terminator, no Calls nor DTOs and STOs except for the current incoming
* to the Terminator will be present in the underlying protocol.
*/
MINIMAL
}
/**
* A base Wrapper for {@link MatsInitiate}, which simply implements MatsInitiate, takes a MatsInitiate instance and
* forwards all calls to that. Use this if you need to wrap the MatsInitiate, where most of the methods are
* pass-through to the target, as any changes to the MatsInitiate interface then won't break your wrapper.
*/
class MatsInitiateWrapper implements MatsWrapper, MatsInitiate {
/**
* This field is private - all methods invoke {@link #unwrap()} to get the instance, which you should too if you
* override any methods. If you want to take control of the wrapped MatsFactory instance, then override
* {@link #unwrap()}.
*/
private MatsInitiate _targetMatsInitiate;
/**
* Standard constructor, taking the wrapped {@link MatsInitiate} instance.
*
* @param targetMatsInitiate
* the {@link MatsFactory} instance which {@link #unwrap()} will return (and hence all forwarded
* methods will use).
*/
public MatsInitiateWrapper(MatsInitiate targetMatsInitiate) {
setWrappee(targetMatsInitiate);
}
/**
* No-args constructor, which implies that you either need to invoke {@link #setWrappee(MatsInitiate)} before
* publishing the instance (making it available for other threads), or override {@link #unwrap()} to provide the
* desired {@link MatsInitiate} instance. In these cases, make sure to honor memory visibility semantics - i.e.
* establish a happens-before edge between the setting of the instance and any other threads getting it.
*/
public MatsInitiateWrapper() {
/* no-op */
}
/**
* Sets the wrapped {@link MatsInitiate}, e.g. in case you instantiated it with the no-args constructor. Do
* note that the field holding the wrapped instance is not volatile nor synchronized. This means that if you
* want to set it after it has been published to other threads, you will have to override both this method and
* {@link #unwrap()} to provide for needed memory visibility semantics, i.e. establish a happens-before edge
* between the setting of the instance and any other threads getting it. A volatile
field would
* work nice.
*
* @param targetMatsInitiate
* the {@link MatsInitiate} which is returned by {@link #unwrap()}, unless that is overridden.
*/
public void setWrappee(MatsInitiate targetMatsInitiate) {
_targetMatsInitiate = targetMatsInitiate;
}
/**
* @return the wrapped {@link MatsInitiate}. All forwarding methods invokes this method to get the wrapped
* {@link MatsInitiate}, thus if you want to get creative wrt. how and when the MatsInitiate is decided,
* you can override this method.
*/
public MatsInitiate unwrap() {
if (_targetMatsInitiate == null) {
throw new IllegalStateException("MatsInitiator.MatsInitiateWrapper.unwrap():"
+ " The '_targetMatsInitiate' is not set!");
}
return _targetMatsInitiate;
}
@Override
public MatsInitiate unwrapFully() {
return unwrap().unwrapFully();
}
@Override
public MatsInitiate traceId(CharSequence traceId) {
unwrap().traceId(traceId);
return this;
}
@Override
public MatsInitiate keepTrace(KeepTrace keepTrace) {
unwrap().keepTrace(keepTrace);
return this;
}
@Override
public MatsInitiate nonPersistent() {
unwrap().nonPersistent();
return this;
}
@Override
public MatsInitiate nonPersistent(long timeToLiveMillis) {
unwrap().nonPersistent(timeToLiveMillis);
return this;
}
@Override
public MatsInitiate interactive() {
unwrap().interactive();
return this;
}
@Override
public MatsInitiate noAudit() {
unwrap().noAudit();
return this;
}
@Override
public MatsInitiate from(String initiatorId) {
unwrap().from(initiatorId);
return this;
}
@Override
public MatsInitiate to(String endpointId) {
unwrap().to(endpointId);
return this;
}
@Override
public MatsInitiate replyTo(String endpointId, Object replySto) {
unwrap().replyTo(endpointId, replySto);
return this;
}
@Override
public MatsInitiate replyToSubscription(String endpointId, Object replySto) {
unwrap().replyToSubscription(endpointId, replySto);
return this;
}
@Override
public MatsInitiate setTraceProperty(String propertyName, Object propertyValue) {
unwrap().setTraceProperty(propertyName, propertyValue);
return this;
}
@Override
public MatsInitiate logMeasurement(String metricId, String metricDescription, String baseUnit, double measure,
String... labelKeyValue) {
unwrap().logMeasurement(metricId, metricDescription, baseUnit, measure, labelKeyValue);
return this;
}
@Override
public MatsInitiate logTimingMeasurement(String metricId, String metricDescription, long nanos,
String... labelKeyValue) {
unwrap().logTimingMeasurement(metricId, metricDescription, nanos, labelKeyValue);
return this;
}
@Override
public MatsInitiate addBytes(String key, byte[] payload) {
unwrap().addBytes(key, payload);
return this;
}
@Override
public MatsInitiate addString(String key, String payload) {
unwrap().addString(key, payload);
return this;
}
@Override
public MessageReference request(Object requestDto) {
return unwrap().request(requestDto);
}
@Override
public MessageReference request(Object requestDto, Object initialTargetSto) {
return unwrap().request(requestDto, initialTargetSto);
}
@Override
public MessageReference send(Object messageDto) {
return unwrap().send(messageDto);
}
@Override
public MessageReference send(Object messageDto, Object initialTargetSto) {
return unwrap().send(messageDto, initialTargetSto);
}
@Override
public MessageReference publish(Object messageDto) {
return unwrap().publish(messageDto);
}
@Override
public MessageReference publish(Object messageDto, Object initialTargetSto) {
return unwrap().publish(messageDto, initialTargetSto);
}
@Override
public void unstash(byte[] stash, Class replyClass, Class stateClass, Class incomingClass,
ProcessLambda lambda) {
unwrap().unstash(stash, replyClass, stateClass, incomingClass, lambda);
}
@Override
public Optional getAttribute(Class type, String... name) {
return unwrap().getAttribute(type, name);
}
@Override
public String toString() {
return this.getClass().getSimpleName() + "[" + unwrap().toString() + "]";
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy