2022.02.07
In a typical Java program, the status of a method call is typically reported in one of three ways:
One result of this is that every API has different error reporting semantics. We have all seen code similar to these examples:
var sender = new EmailSender();
var result = sender.send(email);
if (result != null)
{
logger.error(result);
}
if (!sender.send(email))
{
logger.error("Could not send");
}
var error = sender.send(email);
if (error < 0)
{
logger.error("Couldn't send email: Error #" + error);
}
var sender = new EmailSender();
try
{
sender.send(email);
}
catch (SendException e)
{
logger.error("Unable to send", e);
}
try
{
sender.send(email);
}
catch (SendException e)
{
throw new EmailException("Couldn't send email", e);
}
An alternative to these common idioms is to report status “out-of-band” by broadcasting messages to interested listeners. This has a few immediate advantages:
This status reporting model is implemented by the Java Open Source microservices framework, KivaKit. In KivaKit, our EmailSender class would implement Repeater (which is a kind of Broadcaster) by extending BaseRepeater or (more likely) BaseComponent.
class EmailSender extends BaseRepeater
{
Connector connector = listenTo(new Connector());
boolean send(Email email)
{
if (!connector.connect())
{
problem("Could not send");
return false;
}
[...]
return true;
}
}
class Connector extends BaseRepeater
{
boolean connect()
{
if ( [...] )
{
problem("Cannot connect to server");
}
}
}
class Client extends BaseRepeater
{
EmailSender sender = listenTo(new EmailSender());
void sendReport()
{
sender.send(composeReport());
}
}
Here, we can see that the Connector object created during construction is passed to listenTo(). The listenTo() method, inherited from BaseRepeater, causes EmailSender to listen for any messages broadcast by Connector. Since EmailSender is a Repeater, it will repeat any messages it receives from Connector to its own listeners in Client, forming a listener chain:
Connector ==> EmailSender ==> Client ==> [...]
Note: In KivaKit, broadcasting messages does not involve message queueing or concurrency. Messages are delivered to listeners synchronously via the method Listener.onMessage().
What’s important here is that Connector and EmailSender only report problems regarding their own status. If Connector is unable to connect(), it simply calls problem() explaining what it couldn’t do. The problem() method then transmits a Problem message to EmailSender, and EmailSender in turn transmits it to Client. In this way, EmailSender indirectly provides “out-of-band” information about Connector’s status to Client. Of course, EmailSender also broadcasts its own Problem message if the Connector.connect() method fails, without needing to worry about reporting why the connection failed.
The flow of control for KivaKit messaging is shown in this UML sequence diagram:
Client calls EmailSender.send(), which calls Connector.connect(). During the execution of each of these methods, status messages may be transmitted down the listener chain (as shown by the orange lines) when a method like problem() is called.
The rules of listener chains are:
Use of EmailSender in Client now looks like this:
var sender = listenTo(new EmailSender());
if (!sender.send(email))
{
problem("Unable to send report: $", email);
return false;
}
return true;
Because the semantics of KivaKit status reporting are so regular, it’s possible to condense this idiom even further:
return isTrueOr(sender.send(), "Unable to send report $", email);
The isTrueOr() method here is inherited from the LanguageTrait interface via the Component interface. If the boolean argument to isTrueOr() is not true, it broadcasts the message specified by the remaining arguments as a Problem. It then returns the boolean value. This eliminates a lot of boilerplate if/else nesting when checking multiple conditions. Code like this (23 lines):
if (isTimeToSend())
{
if (email != null)
{
if (sender.send())
{
information("Email sent.");
return true;
}
else
{
problem("Unable to send report");
}
}
else
{
problem("No email to send");
}
}
else
{
problem("Not time to send yet");
}
can be reduced to just this (8 lines):
if (isTrueOr(isTimeToSend(), "Not time to send yet") &&
isNonNullOr(email, "No email to send") &&
isTrueOr(sender.send(email), "Unable to send report"))
{
information("Email sent.");
return true;
}
return false;
KivaKit has hundreds of Listeners and Repeaters. This includes all KivaKit Components. To give an idea of the range of Listener implementations:
Listener | Purpose |
---|---|
Logger | Log status messages |
MutableCount | Count status messages |
MessageAlarm | Trigger an alarm if there are too many problems |
StatusPanel | Show status messages in a Swing panel |
MessageList | Capture messages in a list |
ValidationIssues | Capture problems during validation |
Converter | Broadcast conversion errors |
ThrowingListener | Throws an exception when a problem is received |
ConsoleWriter | Writes status messages directly to the console |
MicroservletRequest | Captures microservlet request handling errors |
Application | Terminal listener that logs and counts messages |
In this short article we took a look at the advantages of broadcaster/listener messaging in status reporting over more typical idioms. We then took a look at how KivaKit implements this system. More information on KivaKit messaging is available on my blog as well as on GitHub.
Questions? Comments? Tweet yours to @OpenKivaKit or post here: