
net.sf.eBus.client.ERequestMonitorFeed Maven / Gradle / Ivy
//
// Copyright 2024 Charles W. Rapp
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
package net.sf.eBus.client;
import javax.annotation.Nullable;
import net.sf.eBus.client.EClient.ClientLocation;
import net.sf.eBus.logging.AsyncLoggerFactory;
import net.sf.eBus.messages.EMessage;
import net.sf.eBus.messages.EReplyMessage;
import net.sf.eBus.messages.ERequestMessage;
import net.sf.eBus.util.Validator;
import org.slf4j.Logger;
/**
* {@code ERequestMonitorFeed} is the application entry point
* for monitoring {@link ERequestMessage request messages} to and
* {@link EReplyMessage reply messages} from local
* {@link EReplier repliers}. This feed cannot be used to monitor
* messages between remote requesters and repliers. Follow these
* steps to use this feed when monitoring requests and replies:
*
* Step 1: Implement
* {@link ERequestMonitor} interface.
*
*
* Step 2:
* {@link ERequestMonitorFeed.Builder Build} request monitor
* for {@code ERequestMonitor} instance and
* {@link net.sf.eBus.messages.EMessageKey type+topic request message key}.
* Note that the feed scope is set to {@code FeedScope.LOCAL} and
* cannot be overridden.
*
*
* Use
* {@link Builder#requestCallback(MessageCallback)},
* {@link Builder#replyCallback(MessageCallback)}, and
* {@link Builder#cancelCallback(MessageCallback)} to set Java
* lambda expressions used in place of {@link ERequestMonitor}
* interface methods.
*
*
* Step 3:
* {@link #monitor() Start monitoring} requests to and replies
* from local repliers. Please note that unlike other eBus roles
* (publisher, subscriber, etc.) the request monitor is
* not informed when the request feed is up or down.
* That is because the monitor is not part of this conversation
* but is standing to the side watching the message flow.
*
*
* Step 4:
* {@code ERequestMonitor} receives request messages, reply
* messages, and cancellations flowing between requesters and
* local repliers.
*
*
* Step 5: When
* request monitor is shutting down,
* {@link ERequestMonitorFeed#unmonitor() stop} monitoring and
* {@link ERequestMonitorFeed#close() close} the monitor feed.
*
* Example use of ERequestMonitorFeed
* import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
import net.sf.eBus.client.ERequestMonitor;
import net.sf.eBus.client.ERequestMonitorFeed;
import net.sf.eBus.client.EScheduledExecutor;
import net.sf.eBus.client.IETimer;
import net.sf.eBus.messages.EMessageKey;
import net.sf.eBus.messages.EReplyMessage;
import net.sf.eBus.messages.ERequestMessage;
Step 1: Implement ERequestMonitor interface.
public class CatalogRequestMonitor implements ERequestMonitor {
private static final Duration REPLY_DELAY = Duration.ofHours(3L);
private final EMessageKey mKey;
private final Map<String, RequestInfo> mOrders;
private ERequestMonitorFeed mFeed;
private IETimer mReplyTimer;
public CatalogRequestMonitor(final String subject) {
mKey = new EMessageKey(com.acme.CatalogOrder.class, subject);
mOrders = new HashMap<>();
}
@Override public void startup() {
try
{
final EScheduledExecutor executor = EScheduledExecutor.getExecutor("blocking");
Step 2: Open a request monitor feed for request message key. May override ERequestMonitor interfaces methods.
mFeed = (ERequestMonitor.builder()).target(this)
.messageKey(mKey)
.build();
Step 3: Start monitoring.
mFeed.monitor();
mReplyTimer = executor.scheduleAtFixedRate(this::onReplyTimeout, this, REPLY_DELAY, REPLY_DELAY);
} catch (IllegalArgumentException argex) {
// Monitoring failed. Place recovery code here.
}
}
Step 4: Wait for requests, replies, and cancellations to arrive.
@Override public void request(final ERequestMessage request, final String replier) {
final CatalogOrder order = (CatalogOrder) request;
// No need to synchronize orders map since all callbacks are on this object's dispatcher.
mOrders.put(order.orderId, new RequestInfo(order, replier));
}
@Override public void reply(final EReplyMessage reply, final String replier) {
final CatalogOrderReply orderReply = (CatalogOrderReply) reply;
final String orderId = orderReply.orderId;
// Is this the final reply?
if (orderReply.isFinal) {
// Yes, stop monitoring the order.
mOrders.remove(orderId);
} else {
// No, update order timestamp since replier is actively working this order.
(mOrders.get(orderId)).updateTimestamp();
}
}
@Override public void cancel(final ERequestMessage request, final String replier) {
final CatalogOrder order = (CatalogOrder) request;
// Remove this order since it is now defunct.
mOrders.remove(order.orderId);
}
@Override public void shutdown() {
mOrders.clear();
Step 5: When shutting down, either unmonitor or close request monitor feed.
if (mFeed != null) {
mFeed.close();
mFeed = null;
}
}
private void onReplyTimeout() {
// Iterate over orders looking for failed replies.
for (OrderInfo oInfo : mOrders.values()) {
// If timed out waiting for a reply, raise an alarm.
}
}
}
*
* Please note: there currently is no matching
* multi-request monitor feed.
*
*
* @see ERequestMonitor
*
* @author Charles W. Rapp
*/
public final class ERequestMonitorFeed
extends ESingleFeed
implements IERequestMonitorFeed
{
//---------------------------------------------------------------
// Member data.
//
//-----------------------------------------------------------
// Constants.
//
/**
* {@link ERequestMonitor#request(ERequestMessage, EReplier)}
* method name is {@value}.
*/
private static final String REQUEST_METHOD = "request";
/**
* {@link ERequestMonitor#reply(EReplyMessage, EReplier)}
* method name is {@value}.
*/
private static final String REPLY_METHOD = "reply";
/**
* {@link ERequestMonitor#cancel(ERequestMessage, int)}
* method name is {@value}.
*/
private static final String CANCEL_METHOD = "cancel";
//-----------------------------------------------------------
// Statics.
//
/**
* Logging subsystem interface.
*/
private static final Logger sLogger =
AsyncLoggerFactory.getLogger();
//-----------------------------------------------------------
// Locals.
//
/**
* Deliver request messages to this callback.
*/
private final MessageCallback mRequestCallback;
/**
* Delivery reply messages to this callback.
*/
private final MessageCallback mReplyCallback;
/**
* Deliver cancel request messages to this callback.
*/
private final MessageCallback mCancelCallback;
//---------------------------------------------------------------
// Member methods.
//
//-----------------------------------------------------------
// Constructors.
//
/**
* Creates a new request monitor instance instance based on
* builder configuration. Note that builder configuration is
* guaranteed to be valid prior to calling this constructor.
* @param builder contains valid request monitor feed
* configuration.
*/
private ERequestMonitorFeed(final Builder builder)
{
super (builder);
mRequestCallback = builder.mRequestCallback;
mReplyCallback = builder.mReplyCallback;
mCancelCallback = builder.mCancelCallback;
} // end of ERequestMonitorFeed(Builder)
//
// end of Constructors.
//-----------------------------------------------------------
//-----------------------------------------------------------
// IERequestMonitorFeed Interface Implementation.
//
/**
* Always returns zero because request monitors do not have
* the concept of activation.
* @param loc contra-feed location.
* @param fs contra-feed state.
* @return zero.
*/
@Override
/* package */ int updateActivation(final ClientLocation loc,
final EFeedState fs)
{
return (0);
} // end of updateActivation(ClientLocation, FeedState)
/**
* If request monitor is in place, then retract this monitor
* feed.
*
* @see EFeed#close()
*/
@Override
protected void inactivate()
{
if (mInPlace)
{
((ERequestSubject) mSubject).unmonitor(this);
mInPlace = false;
}
} // end of inactivate()
/**
* Adds this request monitor feed to associated request
* subject. If this monitor feed is currently in place, then
* does nothing.
* @throws IllegalStateException
* if this feed is closed.
*
* @see #unmonitor()
* @see EFeed#close()
*/
@Override
public void monitor()
{
// Is this feed still active?
if (!mIsActive.get())
{
// No, cannot start monitoring on an inactive feed.
throw (new IllegalStateException(FEED_IS_INACTIVE));
}
// Feed is still active. Is monitoring already in place?
if (!mInPlace)
{
// No, start monitoring now.
sLogger.debug(
"{} request monitor, feed {}: monitoring {} ({}).",
mEClient.location(),
mEClient.clientId(),
mFeedId,
mSubject.key(),
mScope);
((ERequestSubject) mSubject).monitor(this);
mInPlace = true;
}
} // end of monitor()
/**
* Retracts this request monitor feed from associated request
* subject. Does nothing if this feed is not currently in
* place.
* @throws IllegalStateException
*
* @see #monitor()
* @see EFeed#close()
*/
@Override
public void unmonitor()
{
// Is this feed still active?
if (!mIsActive.get())
{
// No, cannot stop monitoring on an inactive feed.
throw (new IllegalStateException(FEED_IS_INACTIVE));
}
// Feed is still active. Is monitoring in place?
if (mInPlace)
{
// Yes, stop monitoring now.
sLogger.debug(
"{} request monitor, feed {}: monitoring {} ({}).",
mEClient.location(),
mEClient.clientId(),
mFeedId,
mSubject.key(),
mScope);
((ERequestSubject) mSubject).unmonitor(this);
mInPlace = false;
}
} // end of unmonitor()
//
// end of IERequestMonitorFeed Interface Implementation.
//-----------------------------------------------------------
//-----------------------------------------------------------
// Get Methods.
//
/**
* Returns {@code true} if this request monitor feed is both
* open and monitoring; otherwise returns {@code false}.
* @return {@code true} if this request monitor feed is both
* open and monitoring.
*/
public boolean isMonitoring()
{
return (mIsActive.get() && mInPlace);
} // end of isMonitoring()
//
// end of Get Methods.
//-----------------------------------------------------------
/**
* Returns a new {@code ERequestMonitorFeed} builder
* instance. This instance is used to build a single request
* monitor feed instance and should not be used to create
* multiple such feeds.
* @return new request monitor feed builder.
*/
public static Builder builder()
{
return (new ERequestMonitorFeed.Builder());
} // end of builder()
/**
* Forwards request message and replier pair to this feed's
* request monitor.
* @param request inbound request message.
* @param replier target replier's eBus object name.
*/
/* package */ void dispatch(final ERequestMessage request,
final String replier)
{
mEClient.dispatch(new MonitorTask<>(request,
replier,
mRequestCallback,
this));
} // end of dispatch(ERequestMessage, String)
/**
* Forwards reply message and replier pair to this feed's
* request monitor.
* @param reply outbound reply message.
* @param replier source replier's eBus object name.
*/
/* package */ void dispatch(final EReplyMessage reply,
final String replier)
{
mEClient.dispatch(new MonitorTask<>(reply,
replier,
mReplyCallback,
this));
} // end of dispatch(EReplyMessage, String)
/**
* Forwards request cancellation and replier pair to this
* feed's request monitor.
* @param request canceled request message.
* @param replier target replier's eBus object name.
*/
/* package */ void dispatchCancel(final ERequestMessage request,
final String replier)
{
mEClient.dispatch(new MonitorTask<>(request,
replier,
mCancelCallback,
this));
} // end of dispatchCancel(ERequestMessage, String)
//---------------------------------------------------------------
// Inner classes.
//
/**
* {@code ERequestMonitorFeed.Builder} is used to create an
* {@code ERequestMonitorFeed} instance. A {@code Builder}
* instance is acquired from
* {@link ERequestMonitorFeed#builder()}. The following
* example shows how to create a {@code ERequestMonitorFeed}
* instance using a {@code Builder}. The example also shows
* how {@code ERequestMonitor} interface methods may be
* replaced with lambda expressions.
* @Override public void startup() {
final EMessageKey = new EMessageKey(com.acme.CatalogOrder.class, subject);
final ERequestMonitorFeed feed (ERequestMonitorFeed.builder()).target(this)
.messageKey(key)
.requestCallback(this::onRequest)
.replyCallback(this::onReply)
.cancelCallback(this::onCancel)
.build();
...
}
private void onRequest(final ERequestMessage request, final String replier) {
...
}
private void onReply(final EReplyMessage reply, final String replier) {
...
}
private void onCancel(final ERequestMessage request, final String replier) {
...
}
*
* @see #builder()
*/
public static final class Builder
extends ESingleFeed.Builder
{
//-----------------------------------------------------------
// Member data.
//
//-------------------------------------------------------
// Locals.
//
/**
* Deliver request messages to this callback.
*/
private MessageCallback mRequestCallback;
/**
* Delivery reply messages to this callback.
*/
private MessageCallback mReplyCallback;
/**
* Inform request monitor that a given request is
* canceled.
*/
private MessageCallback mCancelCallback;
//-----------------------------------------------------------
// Member methods.
//
//-------------------------------------------------------
// Constructors.
//
/**
* Creates a new {@code ERequestMonitorFeed} builder
* instance. Note that request monitor feeds have a
* local feed scope which cannot be changed.
*/
private Builder()
{
super (FeedType.REQUEST_MONITOR_FEED,
ERequestMonitorFeed.class);
mScope = FeedScope.LOCAL_ONLY;
mLocation = ClientLocation.LOCAL;
} // end of Builder()
//
// end of Constructors.
//-------------------------------------------------------
//-------------------------------------------------------
// Abstract Method Overrides.
//
/**
* Returns eBus subject for the configured message key.
* @return eBus feed subject.
*/
@Override
protected ESubject getSubject()
{
return (ERequestSubject.findOrCreate(mKey));
} // end of getSubject()
/**
* Verifies that message key is for a request subject and
* that both request and reply callbacks are set.
* @param problems place invalid configuration settings
* in this problems list.
* @return {@code problems} to allow for method chaining.
*/
@Override
protected Validator validate(Validator problems)
{
// If the callbacks were not put in place, then use
// the defaults.
if (mRequestCallback == null &&
mTarget != null &&
isOverridden(REQUEST_METHOD,
ERequestMessage.class,
String.class))
{
mRequestCallback =
((ERequestMonitor) mTarget)::request;
}
if (mReplyCallback == null &&
mTarget != null &&
isOverridden(REPLY_METHOD,
EReplyMessage.class,
String.class))
{
mReplyCallback =
((ERequestMonitor) mTarget)::reply;
}
if (mCancelCallback == null &&
mTarget != null &&
isOverridden(CANCEL_METHOD,
ERequestMessage.class,
String.class))
{
mCancelCallback =
((ERequestMonitor) mTarget)::cancel;
}
return (super.validate(problems)
.requireTrue((mKey != null &&
mKey.isRequest()),
"messageKey",
"messageKey is not a request")
.requireNotNull(mRequestCallback,
"requestCallback")
.requireNotNull(mReplyCallback,
"replyCallback")
.requireNotNull(mCancelCallback,
"cancelCallback"));
} // end of validate(Validator)
/**
* Returns a new {@code ERequestMonitorFeed} instance
* based on {@code this Builder}'s configuration.
* @return new request monitor feed.
*/
@Override
protected ERequestMonitorFeed buildImpl()
{
return (new ERequestMonitorFeed(this));
} // end of buildImpl()
/**
* Returns {@code this Builder} reference.
* @return {@code this Builder} reference.
*/
@Override
protected Builder self()
{
return (this);
} // end of self()
//
// end of Abstract Method Overrides.
//-------------------------------------------------------
//-------------------------------------------------------
// Set Methods.
//
/**
* Throws {@code UnsupportedOperationException} because
* request monitor feeds have local-only scope which may
* not be overridden.
* @param scope feed scope.
* @return {@code this Builder} instance.
* @throws UnsupportedOperationException
* default local-only feed scope may not be overridden.
*/
@Override
public Builder scope(final FeedScope scope)
{
throw (
new UnsupportedOperationException(
"local-only feed scope may not be overridden"));
} // end of scope(FeedScope)
/**
* Puts inbound request callback in place. If {@code cb}
* is not {@code null}, requests will be passed to
* {@code cb} rather than
* {@link ERequestMonitor#request(ERequestMessage, String)}.
* If {@code cb} is {@code null}, then request messages
* are passed to the
* {@link ERequestMonitor#request(ERequestMessage, String)}
* override.
* @param cb request callback. May be {@code null}.
* @return {@code this Builder} instance.
*/
public Builder requestCallback(@Nullable final MessageCallback cb)
{
mRequestCallback = cb;
return (this);
} // end of requestCallback(MessageCallback<>)
/**
* Puts outbound reply callback in place. If {@code cb}
* is not {@code null}, replies will be passed to
* {@code cb} rather than
* {@link ERequestMonitor#reply(EReplyMessage, String)}.
* If {@code cb} is {@code null}, then reply messages
* are passed to the
* {@link ERequestMonitor#reply(EReplyMessage, String)}
* override.
* @param cb reply callback. May be {@code null}.
* @return {@code this Builder} instance.
*/
public Builder replyCallback(@Nullable final MessageCallback cb)
{
mReplyCallback = cb;
return (this);
} // end of replyCallback(MessageCallback<>)
/**
* Puts inbound request cancellation callback in place.
* if {@code cb} is not {@code null}, cancellations will
* be passed to {@code cb} rather than
* {@link ERequestMonitor#cancel(ERequestMessage, String)}.
* If {@code cb} is {@code null}, then request messages
* are passed to the
* {@link ERequestMonitor#cancel(ERequestMessage, String)}
* override.
* @param cb cancellation callback. May be {@code null}.
* @return {@code this Builder} instance.
*/
public Builder cancelCallback(@Nullable final MessageCallback cb)
{
mCancelCallback = cb;
return (this);
} // end of cancelCallback(()
//
// end of Set Methods.
//-------------------------------------------------------
} // end of class Builder
/**
* Task used to for eBus request and reply messages to
* a request monitor instance.
*/
private static final class MonitorTask
extends AbstractClientTask
{
//-----------------------------------------------------------
// Member data.
//
//-------------------------------------------------------
// Locals.
//
/**
* Forward this message to request monitor.
*/
private final M mMessage;
/**
* Request or reply message is either to or from eBus
* replier with this eBus object name.
*/
private final String mReplier;
/**
* Forward message to request monitor using this
* callback.
*/
private final MessageCallback mCallback;
//-----------------------------------------------------------
// Member methods.
//
//-------------------------------------------------------
// Constructors.
//
private MonitorTask(final M message,
final String replier,
final MessageCallback cb,
final ERequestMonitorFeed feed)
{
super (feed);
mMessage = message;
mReplier = replier;
mCallback = cb;
} // end of AbstractMonitorTask(...)
//
// end of Constructors.
//-------------------------------------------------------
//-------------------------------------------------------
// Runnable Interface Implementations.
//
/**
* Forwards message to target object using configured
* callback lambda expression.
*/
@Override
public void run()
{
final Object target = (mFeed.eClient()).target();
sLogger.trace("{}", this);
// Is the request monitor still around?
if (target != null)
{
// Yes, forward message.
try
{
mCallback.call(mMessage, mReplier);
}
catch (Throwable tex)
{
final String reason =
"MonitorTask[{} {}]";
final String className =
(target.getClass()).getName();
if (sLogger.isDebugEnabled())
{
sLogger.warn(reason,
className,
mMessage.key(),
tex);
}
else
{
sLogger.warn(reason,
className,
mMessage.key());
}
}
}
// No, target went bye-bye.
} // end of run()
//
// end of Runnable Interface Implementations.
//-------------------------------------------------------
//-------------------------------------------------------
// Object Method Overrides.
//
@Override
public String toString()
{
return (String.format("MonitorTask[%s, %s]",
mMessage.key(),
mReplier));
} // end of toString()
//
// end of Object Method Overrides.
//-------------------------------------------------------
} // end of class AbstractMonitorTask
} // end of class ERequestMonitorFeed