The Road Towards a Public API (part 2)

In my last post I described the principles guiding the design of the new programmatic API. As I promised last time, in this blog post I would like to give an overview of new API capabilities that this new design would enable.

One downside of having an API that is tightly-coupled with the implementation of the engines is that it is harder to evolve, and it is more difficult to support more than API at the same time.

The new API is "service-oriented", in that each functionality is provided by a component that we called a service. For example a service for evaluating a PMML may be simply:

interface PredictionService {
    DataContext evaluate(Id identifier, DataContext ctx);
}

Each business asset is pointed to using an identifier. The identifier is constructed as a path, which makes it trivially serializable as a string, easy to share and self-descriptive: one identifier contains most of the relevant meta-data about an asset. For example, to refer to a prediction in PMML:

/predictions/my.prediction.id

Moreover, the path structure makes it natural to refer to nested items in the same asset. For example, to refer to a task in a specific process, for a given process instance:

/processes/my.process.id/instances/my.instance.id/tasks/my.task.id

In the following, we will explore future possibilities that the design of the new API enables. Currently NONE of the following features are implemented, nor do we have a timeline for delivery. However, we do plan to explore these capabilities in the future.

I hope that, by giving you this sneak peek, you will find these design choices convincing, too!

DataContext

One of our first encounters in the API is DataContext. A simple interface that denotes an object that can be transformed into another type. In the first post we explored how DataContext may denote both data types such as records, as well of generic key-value pairs (MapDataContext).

The DataContext basically comes with only two constraints

  • the first we already mentioned: DataContext should be convertible into another data context
  • second, the object it denotes should be serializable (into JSON)

These "strong" guarantees are also what enables some of the further extensions that we describe in the following.

Multiple Service Implementations

The service API allows for multiple different interaction models to be provided for the same feature. For instance, a classic model would be a synchronous API:

interface PredictionService {
    DataContext evaluate(Id identifier, DataContext ctx);
}

But nothing prevents engines to opt-in to provide an asynchronous API; for instance:

interface AsyncPredictionService {
    CompletableFuture<DataContext> evaluate(Id identifier, DataContext ctx);
}

or even, message-based; imagine:

messageBus.send(
    new ProcessStart(
        app.get(Processes.class).get($id)), 
        MapDataContext.of(Map.of(...)));
messageBus.subscribe(
    "/processes/$id",
     // ... subscribe and wait for ProcessStarted event ...
)

To the point where a generic interface may be provided too. Each service, may implement an interface that all implementation would respect. For instance:

interface AsyncGenericService {
    CompletableFuture<DataContext> evaluate(Id id, Context ctx);
}
@Inject AsyncGenericService svc; 
var futureCtx = svc.evaluate(app.get(Processes.class).get($id), MapDataContext.of(...));

In fact, because the identifier is self-descriptive, and the prefix always denotes, by construction, the "top-level" type of the resource that it points to:

/processes/...
/predictions/...
/decisions/...
/rule-units/...

…it would be possible to define a "gateway" service, responding to the generic interface above, that would then re-route the actual evaluation to the corresponding service; say:

/processes/...     -> ProcessService
/predictions/...   -> PredictionService
/decisions/...     -> DecisionService
/rule-units/...    -> RuleUnitService

Structured Identifiers for Behavior Binding

In fact, structured, self-descriptive identifiers open a whole lot of interesting use cases. If anything, being able to univocally, consistently address any component in the platform, allows for binding behavior to components in a simple way.

For instance, we may want to listen for the events that the DMN engine produces; then we may declare a listening class, and bind it to the path:

// all processes 
//(currently equivalent to extending ProcessListener)
@Kogito("/processes") 
class MyEventListener {
   void onStart() { … }
}

Now suppose that you want to listen to the behavior of a specific process id

@Kogito("/processes/my.process.id") 
class MyEventListener {
   void onStart() { … }
}

But we saw that paths may refer to sub-components, too. For instance, tasks. Suppose that you want to define the implementation of a service task. Then you may bind behavior to it as such:

// all processes 
//(currently equivalent to extending ProcessListener)
@Kogito("/processes/my.process.id/task/my.task.id") 
class MyTaskHandler {
   @Inject ProcessInstanceId piid;
   SomeResult doSomething(SomeInput in);
}

Local vs Remote ID: Distributed Execution

So far we only described "paths", i.e. "local" identifiers. A local identifier always starts with a leading slash ‘/’ and its prefix indicates the type of resources (e.g. process, DMN, PMML, rules, etc.)

The reason why it is useful for the ID to be a path, is that a path can be "mounted" on a host:port pair, yielding… a URI! For instance, imagine a fictional kogito:// scheme, then we may denote:

kogito://my-app@my.remote.host:54321/processes/my.process.id

to denote a Kogito application called my-app, reachable at my.remote.host on port 54321. Now wouldn’t it be fancy if you were able to invoke my.process.id using the same service interface ?

@Inject AsyncGenericService svc; 
var localId = app.get(Processes.class).get("my.process.id");
var remoteId = RemoteId.of("my-app@my.remote.host:54321", localId);
var futureCtx = 
     svc.evaluate(
         remoteId, 
         MapDataContext.of(...));

the local application may decode the host part, and direct the request to it. The target, may resolve the request locally, by decoding the "local" part; or it may even act as gateway that re-routes the request to another different distributed service.

The identifier is trivially serializable into a string, and the DataContext is by definition serializable as well!

Messages: Designing A Service Interface

In order to allow for such future extensions, interfaces should be kept simple and small.

I would advocate for single-purpose, single-verb methods. Same verbs may apply to different semantics, depending on the structure of the identifier. For instance:

ABORT [kogito://$host:$port]/processesABORT all processes
ABORT /processes/$idABORT the process with ID $id
ABORT /processes/$id/instances/$instance_idABORT the instance $instance_id of process $id 
EVAL /decisions/$id
PAYLOAD:
{  “json-of-data”: …  }
EVAL /decisions/$id
PAYLOAD:
{  “json-of-data”: …  }

The benefit of one such design is that it is easy to translate these verb/id/payload triples into messages to send over the wire. For instance,

For instance, a simple translation scheme could be adopted to naturally map each message onto a cloud event

{
    "specversion" : "1.0",
    "type" : "org.kie.kogito.process.eval",
    "source" : "/processes/my.process.id",
    "id" : "A234-1234-1234",                  // request id
    "time" : "2018-04-05T17:31:00Z",          // timestamp
    "datacontenttype" : "text/json",
    "data" : {                                // data context
       "var1" : "value1",
       "var2" : "value2"
     }
}

Mapping onto HTTP Verbs

But a small API surface is easier to map onto other API vocabularies.

For instance, it may result very easy to map such a set of commands onto HTTP verbs. This design has emerged from the implementation of the REST endpoints that are already available in Kogito today:

POSTEvaluate/Start
DELETEAbort
PATCHUpdate
etc… 

These simple mappings may even allow to provide a "generic" style of API that all Kogito services may accept. This kind of generic REST API, may make it simpler for generic, multi-purpose clients to be developed. In other words, it would enable a style of REST API interaction that is more reminiscent of the KIE Server in v7, but without its pitfalls (for instance, without the specialized marshalling format)

# starts process
POST http://$host:$port/processes/$id   
BODY
{
   "var1" : "value1",
   "var2" : "value2"
}

Conclusions

This concludes our whirlwind tour in all the possible futures of Kogito APIs.

As for plans, and ongoing work, we are currently working on delivering a fully-functional stateful process API, and the stateful rule API will follow. Next, we will work on the new listener interfaces and the binding mechanism that we have described here.

There is quite a bit of work to do, still, but things are looking bright! If you are looking forward to getting your hands on all of this, stay tuned!

0 0 votes
Article Rating
Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments