Design of FORK ============== Author: Bela Ban, Sanne Grinovero JIRA: https://issues.redhat.com/browse/JGRP-1613 The purpose of FORK is (1) to enable multiple light-weight channels (fork-channels) to piggyback messages on an existing channel and (2) to add protocols to an existing channel that are to be used exclusively by the light-weight channel. One might want to add such a protocol to its dedicated light-weight channel to not interfere with the configuration of other light-weight channel users, or to isolate them from other instances of the same protocol. This allows an application to reuse an existing channel (main-channel), e.g. the channel created for Infinispan inside the WildFly application server, without having to configure and create an entirely new channel, which would be overkill (duplicate resources, new ports to maintain). Yet the new fork-channel will be shielded from messages passing up and down the main-channel. This is all done without modifying the configuration of the main channel. So if we have a main-channel, an application might want to require locking (using protocol CENTRAL_LOCK), which is not defined in the main-channel's configuration. This is done by creating a fork-stack in FORK, identified by a fork-id. All messages with a header containing the fork-id will be sent up the stack with the same fork-id. Messages without a header will be sent up the main stack by FORK. An overview is shown below: (main-ch) (fork-ch7) (fork-ch8) (fork-ch10) ^ ^ (fork-ch9) ^ | | ^ | | | | | | ------------- --------- -------------- | |CENTRAL_LOCK| |COUNTER| |CENTRAL_LOCK| ----------------------------------------------- |FORK | id=1 | | id=2 | | id=3 | ------------------------------------------- |FRAG2| ------- | GMS | ------- ... The main-channel is shown to the left. Messages sent down it will *not* have a header. We have 3 fork-stacks: the first with id=1 has a CENTRAL_LOCK protocol and a fork-channel (fork-ch7). The second (with id=2) has the COUNTER protocol on top and 2 fork-channels: fork-ch8 and fork-ch9. The third (with id=3) could be yet another application needing to use CENTRAL_LOCK for its own purposes, but needing to have locking instances strictly isolated from the application using for-ch7 (on id=1): the two instances of CENTRAL_LOCK will be independent, and they will thus not block on acquiring the same locks. Multiplexing of messages ------------------------ When a message is sent down fork-ch9, the fork-channel will add a header with fork-ch-id=9 and fork-stack-id=2. On the receiver's side, FORK looks at the header. If there is no header, it passes the message up the main-stack. If there is a header, it grabs the fork-stack-id and finds the matching fork-stack. If there is no match, an exception will be thrown. Otherwise the message is passed up. In the example fork-stack-id is 2, so the message is passed up to the COUNTER protocol. At the top of the fork-stack (which can btw have more than one protocol), the fork protocol stack finds the fork-channel matching the fork-channel-id. If not found, an exception will be thrown, else the message is passed to the corresponding fork-channel (fork-ch9). There is a map between fork-channel-ids and fork-channels in the fork protocol stack which is used for the dispatching of messages to the correct fork-channel. Lifecycle of fork-channel ------------------------- There are a few ways a fork-channel can be created. All of these can be invoked programmatically, or FORK can be configured declaratively, via XML (see the next section). (1) If the fork-stack (e.g. with id=2) already exists, a new fork-channel is created and inserted into the map of the fork-channel's protocol stack. If a fork-channel with the given id already exists, we can either throw an exception or return the existing channel (TBD). (2) If the fork-stack with the given id doesn't exist, but FORK is present, then we can create the fork-stack and add it to the map in FORK, keyed by fork-stack-id. Then we create or get the fork-channel as described in (1). (3) If FORK doesn't exist, we can create it and insert it into the main protocol stack. To do this, we need to know the location in the stack where the newly created FORK should be inserted, and possibly some configuration fragment (ie. XML) needs to be passed to the create call. The rest is done as in (2). When a fork-channel is created, it is passed the main-channel as a reference. The lifetime of a fork-channel is always less than or equal to the lifetime of the main-channel on which it piggybacks. When the main-channel is closed or disconnected, invoking an operation on the fork-channel (e.g. sending a message or connecting) will throw an exception. The fork-channel will have the same address, logical name and view as the main-channel. When a fork-stack is initialized (init(), start()), we need to make sure it is disconnected from the main stack, or else the init() and/or start() method will propagate all the way into the main stack, which might cause havoc in some protocols (re-initializing themselves again). So we'll probably create a dummy stub for initialization and starting, or FORK catches these events and handles them. Note that some protocols grab the timer from the transport protocol at init() time, so this needs to be provided by FORK itself. When a fork-channel is disconnected or closed, the corresponding events (DISCONNECT, stop(), destroy()) cannot be propagated down the main-stack, as this would close the main-stack as well, so they need to be caught (by FORK?), similarly to what's done with initialization (see above). When a fork-channel is closed, it is removed from the map in the fork-channel's associated protocol stack. Whether a fork-stack is removed when no more fork-channels reference it is TDB: possibly remove it when it was created programmatically, else leave it when configured declaratively. Configuration of a fork-stack via XML ------------------------------------- If we create fork-stacks programmatically, not all cluster nodes may have the same fork-stacks. For example, if we have a cluster {A,B,C} and only B and C create the fork-stack with id=2, then COUNTER will not work: all requests go to the coordinator (A), however, A doesn't have COUNTER. To overcome this, we can use *declarative configuration* which includes FORK in the main-channel's protocol stack, and creates fork-stacks with id=1 and id=2. This means that fork-stack with id=2 is available on A, B and C, and therefore requests sent to A will be processed. Note that A's fork-stack with id=2 does not need to have a fork-channel created on top; as long as the fork-stack is created, COUNTER will work fine. Declarative configuration would look like this: ... Here we create a main-stack with protocols MFC, UFC, FORK, and FRAG2. Then we create 2 fork-stacks: one with CENTRAL_LOCK and id=1 and another one with COUNTER under id=2. Note that these 2 fork-stacks do *not* include protocol FRAG2. A message with fork-stack-id=2 *ends* in COUNTER (or the corresponding fork-channel if fork-channel-id is set) and does *not* pass through FRAG2. The application could now create fork-channels over an existing fork-stack, or create new fork-stacks and fork-channels over them as well. The problem here is that we would make a big schema change to accommodate such a syntax, so we'll probably just do something like this (TBD): FORK would be configured through an external XML file, which can then define its own schema (similar to what we did in RELAY2). Misc ---- - CREATE_FORK_STACK: install a given fork-stack config in all cluster nodes. Possibly also add a corresponding DELETE_FORK_STACK