Interfaces

This page describes the internal interfaces and APIs within the Unisave system.

🚧 Internal documentation: This documentation page describes Unisave internals and is meant for Unisave developers and very advanced users. Information here might be incomplete, not yet implemented, deprecated, or changed without notice.

Unisave Framework

GitHub: unisave-cloud/framework

From version v0.11.0, the Unisave Framework is being redesigned as a web framework, drawing inspiration from Laravel, express.js, ASP.NET Core, and NancyFx.

The redesign stands on the OWIN standard (Open Web Interface for .NET) and its Microsoft Katana project implementation. These are the two most important technologies since both are referenced and used by the Unisave Framework.

The OWIN standard defines a set of terms:

  1. Server - The HTTP server that directly communicates with the client and then uses OWIN semantics to process requests. Servers may require an adapter layer that converts to OWIN semantics.
  2. Web Framework - A self-contained component on top of OWIN exposing its own object model or API that applications may use to facilitate request processing. Web Frameworks may require an adapter layer that converts from OWIN semantics.
  3. Web Application - A specific application, possibly built on top of a Web Framework, which is run using OWIN compatible Servers.
  4. Middleware - Pass through components that form a pipeline between a server and application to inspect, route, or modify request and response messages for a specific purpose.
  5. Host - The process an application and server execute inside of, primarily responsible for application startup. Some Servers are also Hosts.

Based on these terms, Unisave Framework is both a Web Framework and also a Middleware. The backend application that the game developer creates is a Web Application and is fully loaded and managed by the Unisave Framework. The Server and the Host could be any technology capable of hosting an OWIN application (say the Katana self-host server), or in case of the Unisave cloud, the Unisave Server.

Startup

During startup, the Host creates a Properties dictionary, that is passed to the Application, which uses it to create an Application Delegate (AppFunc) and the Server uses this delegate to handle HTTP requests. The OWIN specification (section 4) is very general. In practise, the Katana project uses the Owin.dll which contains an interface Owin.IAppBuilder and this interface is what facilitates the transfer of the Properties dictionary back and forth and also the construction of the AppFunc delegate.

Moreover, the Application typically defines a class called Startup that is automatically found in the Application assembly by the Owin.Loader system and its Configuration method is invoked with the IAppBuilder instance provided.

// Web Application
public class Startup
{
    public void Configuration(IAppBuilder app)
    {
        // Define the OWIN app here
        app.Properties["..."] = ...;
        app.Use(/* some middleware */)
    }
}

// Host
var appFunc = app.Build();
var httpResponse = appFunc(httpRequest);

Unisave Framework respects this OWIN extension by the Katana project and defines a Unisave.FrameworkStartup class. This class is what should be loaded by the Host.

The framework assembly is also tagged by the Microsoft.Owin.OwinStartupAttribute attribute that states the FrameworkStartup is in fact the startup class to be used, if you want to start the "UnisaveFramework" web application:

[assembly: Microsoft.Owin.OwinStartup(
    "UnisaveFramework",
    typeof(Unisave.FrameworkStartup)
)]

Custom Properties

Unisave Framework expects a set of additional values to be provided by the Host or the Server in the Properties startup dictionary.

unisave.GameAssemblies
A required property of type Assembly[]. Contains all the game backend assemblies that should be used to look up Bootstrappers, Facets, Entities, etc. This must include the Unisave Framework assembly as well, otherwise the Framework will not load properly.

Note: The reason for explicit inclusion of the framework assembly stems from the possibility of not using the Unisave Framework in the backend application. An advanced user could upload a backend code without the framework but with their own startup class and thus utilize a different web framework if they choose so. Another words, the framework is just an internal part of the web application.

unisave.EnvironmentVariables
An optional property of type IDictionary<string, string>. If the application uses environment variables, the values provided here should take precedence. The Unisave Framework initializes the environment variables it provides to the backend application via the Environment.GetEnvironmentVariables method and then extends/overwrites those with values provided in this Property value.

Note: The motivation here is to theoretically allow the hosting of multiple web applications within a single OS process.

HTTP Request

The application can now handle HTTP requests via the middleware components registered to the IAppBuilder instance. How exactly this registration happens is managed by the framework bootstrapping, which is not the scope of this documentation page.

Asynchronicity & Multi-threading

TODO: What about concurrency, multi-threading and asynchronicity?

Error Handling

TODO: How to handle exceptions, 4xx, and 5xx errors. Silence stuff?

Unisave Request

Apart from plain HTTP requests, Unisave defines special types of requests (e.g. facet calls). These requests are identified by the following HTTP request header:

X-Unisave-Request: Facet

If this header is missing, the request is treated as a plain HTTP request. If present with any value, the request is treated as a Unisave Request.

There are following request kinds:

  • Facet - An invocation of a public RPC method from the Facet system.
  • more can be added (e.g. scheduler, job system)

Facet Request

Path

The HTTP request path has the form:

/{facetName}/{methodName}

The facet name may be the short or full name of the class and method name is the exact name of the method to call. So given a facet method MyNamespace.MyFacet.MyMethod(), these are valid ways how to call it:

/MyNamespace.MyFacet/MyMethod
/MyFacet/MyMethod

The prefered variant is the full name (the FullName property of the Type class), and the shortened variant is a human-friendly option.

Request Headers

Content-Type: application/json
The request body must be in the JSON format.

Cookie: unisave_session_id=K1EzKcOGZnjdksmza8Tz
Cookies are used to identify the session, so the client should persist them and send them to the server.

Request Body

The request body contains the arguments to the invoked facet method:

{
    "arguments": [
        42,
        "hello world!",
        {"x": 42, "y": 43, "z": 45}
    ]
}

The number of provided arguments must match the number declared by the method and the values are serialized and de-serialized according to the declared types via the Unisave serializer.

The request body may contain additional values apart from "arguments" in the future.

Status Code

A successful facet execution will result in a 200 status code.

When an exception occurs (inside facet or during facet serach and/or serialization) the status code is still 200. This behaviour is consistent with the JSON-RPC protocol.

A non-200 status code indicates a deeper problem with the delivery of the facet request. This problem might be temporary (such as rate limitting, or server restart), which the client can interpret as having a go-ahead to retry the request at a later time.

Response Headers

Content-Type: application/json
The response body will always be in the JSON format.

Set-Cookie: unisave_session_id=K1EzKcOGZnjdksmza8Tz; expires=Tue, 17-Oct-2023 23:45:02 GMT; Max-Age=7200; path=/; httponly
Cookies are used to identify the session, so the client should persist them and send them to the server. The Set-Cookie header may be sent multiple times - once per each cookie.

Response Body

A successful method invocation:

{
    "status": "ok",
    "returned": ...,
    "logs": [
        {
            "time": "2023-10-18T00:10:24.134Z",
            "level": "info",
            "message": "Hello!",
            "context": null
        }
    ]
}

An exception was thrown:

{
    "status": "exception",
    "exception": {
        "ClassName": "MyException",
        "Message": "Something went wrong!",
        "StackTraceString": "  at Program.Main...",
        ...
    },
    "isKnownException": true,
    "logs": [...]
}

The "status" attribute can be "ok" or "exception" and it states, how the facet method finished its execution. The exception may also be thrown by the surrounding framework code before or after the facet is called.

If there was no exception, the returned value is serialized by the unisave serializer according to the declared return type and sent in the "returned" attribute.

If there is an exception, it's also serialized and sent in the "exception" attribute. There are two kinds of exceptions, those that are expected (say invalid arguments, invalid server state) and those that are unexpected (a bug in the code). Since we don't want bugs to leak information during production, the facet has to define known exception types via C# attributes. Whether the exception is or isn't known (expected) is stored in the "isKnownException" field.

The Unisave Framework by default does not strip away data about unknown exceptions since it expects to be running behind a request gateway that does this stripping for us (so that we can record crashes in production). If you want to strip the data within the framework, you need to modify the facet system bootstrapping code.

The response also contains server-side debug logs as a list of log messages. These make the development within Unity Editor easier. These logs are also stripped away by the request gateway for production builds.

A stripped down response for a production client may look like this:

{
    "status": "exception",
    "exception": {
        "ClassName": "System.Exception",
        "Message": "Internal Server Error",
    },
    "isKnownException": false,
    "logs": []
}

Stopping

The Katana project provides a host.OnAppDisposing value in the Properties dictionary. It's a CancellationToken that is fired the moment the app should terminate. The FrameworkStartup class hooks into this token and triggers app disposal.

Stopping needs to happen synchronously and immediately. There should be no lengthy cleanup.

Unisave Server

GitHub: unisave-cloud/watchdog

A component of the Unisave cloud responsible for hosting the backend applications and providing their oversight.

Note: Formerly known as Unisave Watchdog - taken from the OpenFaaS terminology.

TODO: list possible error responses (initialization crashes, etc...)

Request Gateway

TODO: facet requests target some URL, they need additional data (editor key, build GUID)

TODO: list possible error responses (too many requests, quota exceeded, etc...)