Polysession is under development, everyone is welcome to join the discussion

The core idea of ​​polysession is to convert the communication between client and server into a state machine. This way we can describe the communication protocol in a type-safe way.

Here is an explanation of how this idea works:

Inspired by polystate, higher-order states can be combined, so communication protocols can also be combined. This is a huge improvement, type-safe + composable communication protocols.

These current prototypes are already completed.

GitHub · Where software is built

The ultimate goal of polysession is: protocol-family.

Multiple protocols are run between client and server, and communication is achieved through a TCP connection through network multiplexing.

We can run many protocols, such as heartbeat protocol, network monitoring protocol, control protocol, and various business protocols.

The detailed design of the network multiplexing part is in the second chapter of the following document.

I’m currently stuck here, I can’t seem to complete this step, so I’m looking for anyone interested to join this project!

2 Likes

Maybe you don’t know much about polysession. Here is an example to explain the role of polysession in more detail:

The meaning of this pingpong protocol is as follows:
Initially, both the client and server are in the Start state.

The client checks the value of the client_counter field in the ClientContext.

  1. If the value of client_counter is greater than 10, it clears client_counter and sends a next message to the server. Upon receiving the message, the server clears the server_counter field in the ServerContext. After this, both the client and server enter the new state specified by the next message.

  2. If the value of client_counter is less than 10, the client sends a ping message to the server containing the value. Upon receiving the value, the server stores it in server_counter, increments server_counter by one, and then sends the new server_counter value to the client. The client receives the message and stores it in client_counter. The client and server then enter the Start state again.


pub fn PingPong(Data_: type, State_: type) type {
    return ps.Session("PingPong", Data_, State_);
}

pub const ServerContext = struct {
    server_counter: i32,
};

pub const ClientContext = struct {
    client_counter: i32,
};

pub const Context: ps.ClientAndServerContext = .{
    .client = ClientContext,
    .server = ServerContext,
};

const PongFn = struct {
    pub fn process(ctx: *ServerContext) !i32 {
        ctx.server_counter += 1;
        return ctx.server_counter;
    }

    pub fn preprocess(ctx: *ClientContext, val: i32) !void {
        ctx.client_counter = val;
    }
};

pub fn Start(NextFsmState: type) type {
    return union(enum) {
        ping: PingPong(i32, ps.Cast("pong", .server, PongFn, PingPong(i32, @This()))),
        next: NextFsmState,

        pub const agency: ps.Role = .client;

        pub fn process(ctx: *ClientContext) !@This() {
            if (ctx.client_counter >= 10) {
                ctx.client_counter = 0;
                return .{ .next = .{ .data = {} } };
            }
            return .{ .ping = .{ .data = ctx.client_counter } };
        }

        pub fn preprocess(ctx: *ServerContext, msg: @This()) !void {
            switch (msg) {
                .ping => |val| ctx.server_counter = val.data,
                .next => {
                    ctx.server_counter = 0;
                },
            }
        }
    };
}

const EnterFsmState = PingPong(void, Start(PingPong(void, ps.Exit)));

Here we let next point to the Exit state, which means that the pingpong protocol will exit directly after running.

Run this protocol, the result is as follows:

client:                                                          server:
send: .{ .ping = .{ .data = 0 } }                                recv: .{ .ping = .{ .data = 0 } }
recv: .{ .cast = .{ .data = 1 } }                                send: .{ .cast = .{ .data = 1 } }
send: .{ .ping = .{ .data = 1 } }                                recv: .{ .ping = .{ .data = 1 } }
recv: .{ .cast = .{ .data = 2 } }                                send: .{ .cast = .{ .data = 2 } }
send: .{ .ping = .{ .data = 2 } }                                recv: .{ .ping = .{ .data = 2 } }
recv: .{ .cast = .{ .data = 3 } }                                send: .{ .cast = .{ .data = 3 } }
send: .{ .ping = .{ .data = 3 } }                                recv: .{ .ping = .{ .data = 3 } }
recv: .{ .cast = .{ .data = 4 } }                                send: .{ .cast = .{ .data = 4 } }
send: .{ .ping = .{ .data = 4 } }                                recv: .{ .ping = .{ .data = 4 } }
recv: .{ .cast = .{ .data = 5 } }                                send: .{ .cast = .{ .data = 5 } }
send: .{ .ping = .{ .data = 5 } }                                recv: .{ .ping = .{ .data = 5 } }
recv: .{ .cast = .{ .data = 6 } }                                send: .{ .cast = .{ .data = 6 } }
send: .{ .ping = .{ .data = 6 } }                                recv: .{ .ping = .{ .data = 6 } }
recv: .{ .cast = .{ .data = 7 } }                                send: .{ .cast = .{ .data = 7 } }
send: .{ .ping = .{ .data = 7 } }                                recv: .{ .ping = .{ .data = 7 } }
recv: .{ .cast = .{ .data = 8 } }                                send: .{ .cast = .{ .data = 8 } }
send: .{ .ping = .{ .data = 8 } }                                recv: .{ .ping = .{ .data = 8 } }
recv: .{ .cast = .{ .data = 9 } }                                send: .{ .cast = .{ .data = 9 } }
send: .{ .ping = .{ .data = 9 } }                                recv: .{ .ping = .{ .data = 9 } }
recv: .{ .cast = .{ .data = 10 } }                               send: .{ .cast = .{ .data = 10 } }
send: .{ .next = .{ .data = void } }                             recv: .{ .next = .{ .data = void } }

const EnterFsmState = PingPong(void, Start(PingPong(void, Start(PingPong(void, ps.Exit)))));

Let’s modify the protocol so that next now points to the Start state of pingpong (inside Start’s next points to Exit). This means we will run the pingpong protocol twice.

This is the embodiment of compositionality.

Run this protocol, the result is as follows:

client:                                                          server:                                                     
send: .{ .ping = .{ .data = 0 } }                                recv: .{ .ping = .{ .data = 0 } }    
recv: .{ .cast = .{ .data = 1 } }                                send: .{ .cast = .{ .data = 1 } }    
send: .{ .ping = .{ .data = 1 } }                                recv: .{ .ping = .{ .data = 1 } }    
recv: .{ .cast = .{ .data = 2 } }                                send: .{ .cast = .{ .data = 2 } }    
send: .{ .ping = .{ .data = 2 } }                                recv: .{ .ping = .{ .data = 2 } }    
recv: .{ .cast = .{ .data = 3 } }                                send: .{ .cast = .{ .data = 3 } }    
send: .{ .ping = .{ .data = 3 } }                                recv: .{ .ping = .{ .data = 3 } }    
recv: .{ .cast = .{ .data = 4 } }                                send: .{ .cast = .{ .data = 4 } }    
send: .{ .ping = .{ .data = 4 } }                                recv: .{ .ping = .{ .data = 4 } }    
recv: .{ .cast = .{ .data = 5 } }                                send: .{ .cast = .{ .data = 5 } }    
send: .{ .ping = .{ .data = 5 } }                                recv: .{ .ping = .{ .data = 5 } }    
recv: .{ .cast = .{ .data = 6 } }                                send: .{ .cast = .{ .data = 6 } }    
send: .{ .ping = .{ .data = 6 } }                                recv: .{ .ping = .{ .data = 6 } }    
recv: .{ .cast = .{ .data = 7 } }                                send: .{ .cast = .{ .data = 7 } }    
send: .{ .ping = .{ .data = 7 } }                                recv: .{ .ping = .{ .data = 7 } }    
recv: .{ .cast = .{ .data = 8 } }                                send: .{ .cast = .{ .data = 8 } }    
send: .{ .ping = .{ .data = 8 } }                                recv: .{ .ping = .{ .data = 8 } }    
recv: .{ .cast = .{ .data = 9 } }                                send: .{ .cast = .{ .data = 9 } }    
send: .{ .ping = .{ .data = 9 } }                                recv: .{ .ping = .{ .data = 9 } }    
recv: .{ .cast = .{ .data = 10 } }                               send: .{ .cast = .{ .data = 10 } }    
send: .{ .next = .{ .data = void } }                             recv: .{ .next = .{ .data = void } }    
send: .{ .ping = .{ .data = 0 } }                                recv: .{ .ping = .{ .data = 0 } }    
recv: .{ .cast = .{ .data = 1 } }                                send: .{ .cast = .{ .data = 1 } }    
send: .{ .ping = .{ .data = 1 } }                                recv: .{ .ping = .{ .data = 1 } }    
recv: .{ .cast = .{ .data = 2 } }                                send: .{ .cast = .{ .data = 2 } }    
send: .{ .ping = .{ .data = 2 } }                                recv: .{ .ping = .{ .data = 2 } }    
recv: .{ .cast = .{ .data = 3 } }                                send: .{ .cast = .{ .data = 3 } }    
send: .{ .ping = .{ .data = 3 } }                                recv: .{ .ping = .{ .data = 3 } }    
recv: .{ .cast = .{ .data = 4 } }                                send: .{ .cast = .{ .data = 4 } }    
send: .{ .ping = .{ .data = 4 } }                                recv: .{ .ping = .{ .data = 4 } }    
recv: .{ .cast = .{ .data = 5 } }                                send: .{ .cast = .{ .data = 5 } }    
send: .{ .ping = .{ .data = 5 } }                                recv: .{ .ping = .{ .data = 5 } }    
recv: .{ .cast = .{ .data = 6 } }                                send: .{ .cast = .{ .data = 6 } }    
send: .{ .ping = .{ .data = 6 } }                                recv: .{ .ping = .{ .data = 6 } }    
recv: .{ .cast = .{ .data = 7 } }                                send: .{ .cast = .{ .data = 7 } }    
send: .{ .ping = .{ .data = 7 } }                                recv: .{ .ping = .{ .data = 7 } }    
recv: .{ .cast = .{ .data = 8 } }                                send: .{ .cast = .{ .data = 8 } }    
send: .{ .ping = .{ .data = 8 } }                                recv: .{ .ping = .{ .data = 8 } }    
recv: .{ .cast = .{ .data = 9 } }                                send: .{ .cast = .{ .data = 9 } }    
send: .{ .ping = .{ .data = 9 } }                                recv: .{ .ping = .{ .data = 9 } }    
recv: .{ .cast = .{ .data = 10 } }                               send: .{ .cast = .{ .data = 10 } }    
send: .{ .next = .{ .data = void } }                             recv: .{ .next = .{ .data = void } }    

I think all this is enough to illustrate the power of polysession. You can combine various protocols like building blocks, and it is completely safe and correct.

code code

The simplest file sending protocol

pub fn SendFile(Data_: type, State_: type) type {
    return ps.Session("SendFile", Data_, State_);
}

pub const Start = union(enum) {
    send: SendFile([]const u8, @This()),
    exit: SendFile([]const u8, ps.Exit),

    pub const agency: ps.Role = .server;

    pub fn process(ctx: *ServerContext) !@This() {
        const n = try ctx.reader.readSliceShort(&ctx.send_buff);
        if (n < ctx.send_buff.len) {
            return .{ .exit = .{ .data = ctx.send_buff[0..n] } };
        } else {
            return .{ .send = .{ .data = &ctx.send_buff } };
        }
    }

    pub fn preprocess(ctx: *ClientContext, msg: @This()) !void {
        switch (msg) {
            .send => |val| {
                ctx.recved += val.data.len;
                try ctx.writer.writeAll(val.data);
            },
            .exit => |val| {
                ctx.recved += val.data.len;
                try ctx.writer.writeAll(val.data);
                try ctx.writer.flush();
            },
        }

        std.debug.print("recv: {d:.2}\n", .{
            @as(f32, @floatFromInt(ctx.recved)) / @as(f32, @floatFromInt(ctx.total)),
        });
    }
};

1 Like

A more complex file transfer protocol checks the consistency of the sent and received data after every 30MB transfer. This also involves separately checking the consistency of the last transmitted data.

Using composability and state machines, this is easily accomplished.

pub const ServerContext = struct {
    server_counter: i32,

    send_buff: [1024 * 1024]u8 = @splat(0),
    file_size: u64,
    send_times: i32 = 0,
    hasher: std.hash.XxHash32,
    reader: *std.Io.Reader,
};

pub const ClientContext = struct {
    client_counter: i32,

    recved_hash: ?u32 = null,
    hasher: *std.hash.XxHash32,
    writer: *std.Io.Writer,
    total: u64 = 0,
    recved: u64 = 0,
};


pub fn SendFile(Data_: type, State_: type) type {
    return ps.Session("SendFile", Data_, State_);
}

pub const Start = union(enum) {
    check: SendFile(u32, CheckHash(@This())),
    send: SendFile([]const u8, @This()),
    final: SendFile(
        []const u8,
        ps.Cast(
            "final hash check init",
            .server,
            FinalHashCheckInitFn,
            SendFile(u32, CheckHash(ps.Exit)),
        ),
    ),

    const FinalHashCheckInitFn = struct {
        pub fn process(ctx: *ServerContext) !u32 {
            return ctx.hasher.final();
        }

        pub fn preprocess(ctx: *ClientContext, val: u32) !void {
            ctx.recved_hash = val;
        }
    };

    pub const agency: ps.Role = .server;

    pub fn process(ctx: *ServerContext) !@This() {
        if (@mod(ctx.send_times, 60) == 0) {
            ctx.send_times += 1;
            const curr_hash = ctx.hasher.final();
            ctx.hasher = std.hash.XxHash32.init(0);
            return .{ .check = .{ .data = curr_hash } };
        }

        ctx.send_times += 1;
        const n = try ctx.reader.readSliceShort(&ctx.send_buff);
        if (n < ctx.send_buff.len) {
            ctx.hasher.update(ctx.send_buff[0..n]);
            return .{ .final = .{ .data = ctx.send_buff[0..n] } };
        } else {
            ctx.hasher.update(&ctx.send_buff);
            return .{ .send = .{ .data = &ctx.send_buff } };
        }
    }

    pub fn preprocess(ctx: *ClientContext, msg: @This()) !void {
        switch (msg) {
            .send => |val| {
                ctx.recved += val.data.len;
                try ctx.writer.writeAll(val.data);
            },
            .final => |val| {
                ctx.recved += val.data.len;
                try ctx.writer.writeAll(val.data);
                try ctx.writer.flush();
            },
            .check => |val| {
                ctx.recved_hash = val.data;
            },
        }

        std.debug.print("recv: {d:.2}\n", .{
            @as(f32, @floatFromInt(ctx.recved)) / @as(f32, @floatFromInt(ctx.total)),
        });
    }
};

pub fn CheckHash(NextState: type) type {
    return union(enum) {
        Successed: SendFile(void, NextState),
        Failed: SendFile(void, ps.Exit),

        pub const agency: ps.Role = .client;

        pub fn process(ctx: *ClientContext) !@This() {
            try ctx.writer.flush();

            const curr_hashed = ctx.hasher.final();

            if (curr_hashed == ctx.recved_hash.?) {
                ctx.hasher.* = std.hash.XxHash32.init(0);
                std.debug.print("hash check successed!\n", .{});
                return .{ .Successed = .{ .data = {} } };
            } else {
                std.debug.print("hash check failed!\n", .{});
                return .{ .Failed = .{ .data = {} } };
            }
        }

        pub fn preprocess(ctx: *ServerContext, msg: @This()) !void {
            _ = ctx;
            _ = msg;
        }
    };
}

code:

vide: 【Polysession例子: 带hash较检的文件传输协议】 Polysession例子: 带hash较检的文件传输协议_哔哩哔哩_bilibili

Goals of polysession: a multi-role communication protocol framework that is ahead of its time.

Previously, polysession could only communicate between client and server, but now it can support communication between any number of roles.

Here is an example of a two-phase commit: polysession/examples/two_phase_commit.zig at main · sdzx-1/polysession · GitHub

You can run this demo with the following command

zig build 2pc

This demo involves three roles and retries when things fail.

Polysession is not ahead of its time, and its expressiveness is not as good as HasChor. But in most cases, polysession is also good enough.