netcreateorg / netcreate-itest

Developing the 2.0 version of NetCreate
https://github.com/netcreateorg/netcreate-2018
Other
0 stars 0 forks source link

Infrastructure: Module Architecture Stage 4 - Modular URNET #138

Closed daveseah closed 4 months ago

daveseah commented 4 months ago

This PR adds the rewritten URNET messaging system upon which all our apps rely on to talk to each other. It now supports the following communications interfaces:

It should be extensible to other communications interfaces such as MQTT. Additionally, this modular rewrite opens up several possibilities:

This PR is ready for alpha testing. It does NOT impact NetCreate's current operation, as URSYS is designed to be a standalone modular system with its own modern build tools.

Known Issues

PR Feature Testing

0. INITIALIZING FOR TEST

This will install any new packages

1. LAUNCH ALL THE SERVERS

In the terminal window, type the following:

Your terminal window will look something like this: image

Three different servers have started:

2. TEST UDS and WSS CLIENTS

Now you'll open a second terminal window to run the client tests for UDS and WSS.

This will run a series of URNET message tests. The output in the second terminal will look like this: image

You'll also see that there are connection messages at the bottom of the first terminal window: image

3. TEST HTTP CLIENT

Now switch to your Chrome web browser and open two tabs.

You'll see output that looks something like this: image

4. SHUTDOWN ALL SERVERS

If there is a server crash, using this command in any available VSCODE terminal window will clean things up.

You'll see this in the terminal window you just typed in: image

You'll see a number of shutdown messages in the terminal window where you originally issued the ur net start command: image

5. SHUTDOWN ALL SERVERS (ALTERNATE)

Alternatively, you can also click in the first VSCODE terminal window by pressing CTRL-C to kill the ur net start process.

The SIGINT is sent to the process, which shuts down all the child processes gracefully (I hope): image


Technical Details

The net addon is a modular version of URSYS messaging (aka URNET), which is used to implement our asynchronous data passing between multiple webapps by using the server as the messaging hub. It's similar to other message-based systems like MQTT except that URNET provides a transaction call that returns data.

URNET is one of the key pillars of our web application operations, allowing programmers without experience in network asynchronous programming to work with our data. URNET is the basis for our controllers, rendering system, and coordination between web applications.

URNET Refresher

// EP is the "endpoint" that this code talks to
// using URNET's various network calls (there are local versions too)
EP.netCall('SRV:GET_DATA',{ key: 'foo' }).then( data => console.log('got data',data));
EP.netSend('NET:VOTE',{ pollName:'bananas', choice: 'ok I guess' });
EP.netSignal('NET:NOTICE', { status:'alive', name:'Felix' });
EP.netPing('NET:WHO_IS_THERE').then( uaddrs => console.log('list of addr',uaddrs));

// using URNET's message declaration to receive messages
// data return works only when netCall() is used to invoke the message
EP.registerMessage('SRV:GET_DATA', data => {
  // do something with incoming data, then return it to caller
  return { note: 'sri was here!', ...data };
});

[!TIP] netCall, netPing, and netSend are all asynchronous functions that return a Promise, so they can take advantage of async/await-style programming. netSignal, however, is a synchronous function.

URNET Improvements

Old versions of URNET were delicately coded in Javascript, updated over four different projects over the past 10 years, and it was difficult to "reason" about. The new modular version of URNET is written in Typescript and removes many confusing structures. It also combining many intertwined modules of the old system into three new classes:

Because of the abstraction of NetSocket, to implement a new server type uses very similar code.

Here is the relevant fragment of the WebSocket Server on NodeJS (using the ws library):

const EP = new NetEndpoint();
EP.configAsServer('SRV03'); // hardcode arbitrary server address
const WSS = new WebSocketServer(options, () => {
  WSS.on('connection', (client_link, request) => {
    const send = pkt => client_link.send(pkt.serialize());
    const onData = data => {
      const returnPkt = EP._clientDataIngest(data, client_sock);
      if (returnPkt) client_link.send(returnPkt.serialize());
    };
    const client_sock = new NetSocket(client_link, { send, onData });
    // check if this is a new socket
    if (EP.isNewSocket(client_sock)) {
      EP.addClient(client_sock);
      const uaddr = client_sock.uaddr;
      LOG(`${uaddr} client connected`);
    }
    client_link.on('message', onData);
    ...
  });
});

And this is the fragment establishing client connection, also using the NetEndpoint and NetSocket classes:

EP = new NetEndpoint();
SERVER_LINK = new WebSocket(wsURI);
SERVER_LINK.addEventListener('open', async () => {
  LOG(...PR('Connected to server'));
  const send = pkt => SERVER_LINK.send(pkt.serialize());
  const onData = event => EP._serverDataIngest(event.data, client_sock);
  const client_sock = new NetSocket(SERVER_LINK, { send, onData });
  SERVER_LINK.addEventListener('message', onData);
  ...
}); 

In particular, these are the lines that are the bridge between the underlying socket tech and NetEndpoint:

  // WSS server managing connections 
  const send = pkt => client_link.send(pkt.serialize());
  const onData = data => {
      const returnPkt = EP._clientDataIngest(data, client_sock);
      if (returnPkt) client_link.send(returnPkt.serialize());
  };
  client_link.on('message', onData);

  // client using WebSocket SERVER_LINK
  const send = pkt => SERVER_LINK.send(pkt.serialize());
  const onData = event => EP._serverDataIngest(event.data, client_sock);
  const client_sock = new NetSocket(SERVER_LINK, { send, onData });
  SERVER_LINK.addEventListener('message', onData);

For comparison, here are the same lines that are used in the Unix Domain Socket (UDS) version, which uses slightly different semantics (e.g. write instead of send) than ws WebSockets

  // UDS server managing connections
  const send = pkt => client_link.write(pkt.serialize());
  const onData = data => {
    const returnPkt = EP._clientDataIngest(data, socket);
    if (returnPkt) client_link.write(returnPkt.serialize());
  };
  const socket = new NetSocket(client_link, { send, onData });
  client_link.on('data', onData);

  // UDS client
  const send = pkt => SERVER_LINK.write(pkt.serialize());
  const onData = data => EP._serverDataIngest(data, client_sock);
  const client_sock = new NetSocket(SERVER_LINK, { send, onData });
  SERVER_LINK.on('data', onData);

[!NOTE] The methods EP._clientDataIngest and EP._serverDataIngest are intentioned named to suggest they are mirror functions, as the direction of the send() and onData() functions depends on whether you're a server or a client. This can be really confusing without the naming patterns here.

URNET Protocol Handshaking

The new version of the URNET protocol now has an official way to authenticate and register client details, as well as a new declare for dynamically-changing aspects of Endpoints. These are special NetPacket types that are recognized by Endpoints configured as a server.

When a client connects to a server, the following steps are undertaken by the client:

  1. It calls EP.connectAsClient(client_sock, auth) which sends an authentication packet to the server. On connection, the client now as an address but it is not yet able to send/receive messages.
  2. It calls EP.registerClient(info) to define what kind of client it is. After this step, the client is considered to to be connected to URNET and can send/receive data
  3. If the client has messages it wants to register, it calls EP.clientDeclare({msg_list}) to upload what messages it can receive. The server updates this information whenever it is received, so clients can change their message lists.

Previously, there was no formal system for managing authentication and client registration, and it is now baked-into the connection logic. All other services can be implemented using regular messaging.

[!TIP] For those unfamiliar with URSYS, the messaging system does not rely on addresses to "target" a specific webapp. The message names themselves are considered the address. If there are multiple implementors of a message, all of them will receive it. If data is returned, you will receive an array of all the data from each implementor. In general, URSYS-based apps should be designed around logical message names and groups, not addresses and identities.

Short Term Directions

Long Term Directions

Additional Documentation

See the WIKI hosted on the standalone repo for the URSYS library. This is a work in progress.

benloh commented 4 months ago

Confirmed that this works as described. Confirmed that app still runs.

Should I be converting the UDATA calls in Net.Create to EP calls?

(Note minor edit: The "2. TEST UDS and WSS CLIENTS" call should be ur net client not ur net clients

dsriseah commented 4 months ago

The new URSYS is not a replacement for UNISYS in NetCreate, as it uses a different internal system and is incompatible.