// Copyright (c) Vitaliy Filippov, 2019+
// License: VNPL-1.1 (see README.md for details)
//
// NFS RDMA support

#ifdef WITH_RDMACM

#define _XOPEN_SOURCE

#include <rdma/rdma_cma.h>
#include <fcntl.h>

#include "addr_util.h"

#include "proto/nfs.h"
#include "proto/rpc.h"
#include "proto/rpc_rdma.h"

#include "nfs_proxy.h"

#include "rdma_alloc.h"

#define NFS_RDMACM_PRIVATE_DATA_MAGIC_LE 0x180eabf6

struct __attribute__((__packed__)) nfs_rdmacm_private
{
    uint32_t format_identifier; // magic, should be 0xf6ab0e18 in big endian
    uint8_t version;            // version, 1
    uint8_t remote_invalidate;  // remote invalidation flag (1 or 0)
    uint8_t max_send_size;      // maximum RDMA Send operation size / 1024 - 1 (i.e. 0 is 1 KB, 255 is 256 KB)
    uint8_t max_recv_size;      // maximum RDMA Receive operation size / 1024 - 1 (i.e. 0 is 1 KB, 255 is 256 KB)
};

struct nfs_rdma_buf_t
{
    void *buf = NULL;
    size_t len = 0;
    ibv_mr *mr = NULL;
};

struct nfs_rdma_conn_t;

struct nfs_rdma_dev_state_t
{
    rdma_cm_id *cmid = NULL;
    ibv_device_attr dev_attr;
    ibv_comp_channel *channel = NULL;
    ibv_cq *cq = NULL;
    rdma_allocator_t *alloc = NULL;
    int max_cqe = 0;

    static nfs_rdma_dev_state_t *create(rdma_cm_id *cmid, uint64_t rdma_malloc_round_to, uint64_t rdma_max_unused_buffers);
    ~nfs_rdma_dev_state_t();
};

nfs_rdma_dev_state_t *nfs_rdma_dev_state_t::create(rdma_cm_id *cmid, uint64_t rdma_malloc_round_to, uint64_t rdma_max_unused_buffers)
{
    nfs_rdma_dev_state_t *self = new nfs_rdma_dev_state_t;
    self->cmid = cmid;
    int r = ibv_query_device(cmid->verbs, &self->dev_attr);
    if (r != 0)
    {
        fprintf(stderr, "Failed to query listerning RDMA device: %s (code %d)\n", strerror(r), r);
        delete self;
        return NULL;
    }
    self->channel = ibv_create_comp_channel(cmid->verbs);
    if (!self->channel)
    {
        fprintf(stderr, "Couldn't create RDMA completion channel\n");
        delete self;
        return NULL;
    }
    self->max_cqe = 4096;
    self->cq = ibv_create_cq(cmid->verbs, self->max_cqe, NULL, self->channel, 0);
    if (!self->cq)
    {
        fprintf(stderr, "Couldn't create RDMA completion queue\n");
        delete self;
        return NULL;
    }
    self->alloc = rdma_malloc_create(cmid->pd, rdma_malloc_round_to, rdma_max_unused_buffers, IBV_ACCESS_LOCAL_WRITE);
    fcntl(self->channel->fd, F_SETFL, fcntl(self->channel->fd, F_GETFL, 0) | O_NONBLOCK);
    return self;
}

nfs_rdma_dev_state_t::~nfs_rdma_dev_state_t()
{
    if (cq)
    {
        ibv_destroy_cq(cq);
        cq = NULL;
    }
    if (channel)
    {
        ibv_destroy_comp_channel(channel);
        channel = NULL;
    }
    if (alloc)
    {
        rdma_malloc_destroy(alloc);
        alloc = NULL;
    }
}

struct nfs_rdma_context_t
{
    std::string bind_address;
    int rdmacm_port = 0;
    uint32_t max_iodepth = 16, max_send_wr = 1024;
    uint64_t rdma_malloc_round_to = 1048576, rdma_max_unused_buffers = 64*1048576;
    uint64_t max_send_size = 256*1024, max_recv_size = 256*1024;

    nfs_proxy_t *proxy = NULL;
    epoll_manager_t *epmgr = NULL;

    rdma_event_channel *rdmacm_evch = NULL;
    rdma_cm_id *listener_id = NULL;
    std::map<rdma_cm_id*, nfs_rdma_conn_t*> rdma_connections;

    ~nfs_rdma_context_t();
    void handle_rdmacm_events();
    void rdmacm_accept(rdma_cm_event *ev);
    void rdmacm_established(rdma_cm_event *ev);
};

struct nfs_rdma_conn_t
{
    nfs_rdma_context_t *ctx = NULL;
    nfs_client_t *client = NULL;
    nfs_rdma_dev_state_t *conn_dev = NULL;
    rdma_cm_id *id = NULL;
    int max_send_size = 256*1024, max_recv_size = 256*1024;
    int max_buf_size = 256*1024;
    int remote_max_send_size = 1024, remote_max_recv_size = 1024;
    int max_rdma_reads = 16;
    bool remote_invalidate = true;
    bool established = false;
    uint32_t cur_credit = 16;
    uint32_t cur_send = 0;
    uint32_t cur_rdma_reads = 0;
    std::vector<nfs_rdma_buf_t> recv_buffers;
    std::map<void*, nfs_rdma_buf_t> used_buffers;
    int next_recv_buf = 0;
    std::vector<rpc_op_t*> outbox;
    std::vector<int> outbox_wrs;
    std::vector<rpc_op_t*> chunk_inbox, chunk_read_postponed;
    int outbox_pos = 0;

    void handle_io();
    void post_initial_receives();
    ~nfs_rdma_conn_t();
    nfs_rdma_buf_t create_buf(size_t len);
    void post_recv(nfs_rdma_buf_t b);
    void post_send();
    int post_chunk_reads(rpc_op_t *rop, bool push);
    bool handle_recv(void *buf, size_t len);
    void free_rdma_rpc_op(rpc_op_t *rop);
    void reuse_buffer(void *buf);
    void rdma_encode_header(XDR *xdrs, rpc_op_t *rop, bool nomsg);
};

nfs_rdma_context_t* nfs_proxy_t::create_rdma(const std::string & bind_address, int rdmacm_port,
    uint32_t max_iodepth, uint32_t max_send_wr, uint64_t rdma_malloc_round_to, uint64_t rdma_max_unused_buffers)
{
    nfs_rdma_context_t* self = new nfs_rdma_context_t;
    self->proxy = this;
    self->epmgr = epmgr;
    self->bind_address = bind_address;
    self->rdmacm_port = rdmacm_port;
    self->max_iodepth = max_iodepth;
    self->max_send_wr = max_send_wr ? max_send_wr : 1024;
    self->rdma_malloc_round_to = rdma_malloc_round_to;
    self->rdma_max_unused_buffers = rdma_max_unused_buffers;
    self->rdmacm_evch = rdma_create_event_channel();
    if (!self->rdmacm_evch)
    {
        fprintf(stderr, "Failed to initialize RDMA-CM event channel: %s (code %d)\n", strerror(errno), errno);
        delete self;
        return NULL;
    }
    fcntl(self->rdmacm_evch->fd, F_SETFL, fcntl(self->rdmacm_evch->fd, F_GETFL, 0) | O_NONBLOCK);
    epmgr->tfd->set_fd_handler(self->rdmacm_evch->fd, false, [self](int rdmacm_eventfd, int epoll_events)
    {
        self->handle_rdmacm_events();
    });
    int r = rdma_create_id(self->rdmacm_evch, &self->listener_id, NULL, RDMA_PS_TCP);
    if (r != 0)
    {
        fprintf(stderr, "Failed to create RDMA-CM ID: %s (code %d)\n", strerror(errno), errno);
        delete self;
        return NULL;
    }
    sockaddr_storage addr;
    if (!string_to_addr(bind_address, 0, rdmacm_port, &addr))
    {
        fprintf(stderr, "Server address: %s is not valid\n", bind_address.c_str());
        delete self;
        return NULL;
    }
    r = rdma_bind_addr(self->listener_id, (sockaddr*)&addr);
    if (r != 0)
    {
        fprintf(stderr, "Failed to bind RDMA-CM to %s:%d: %s (code %d)\n", bind_address.c_str(), rdmacm_port, strerror(errno), errno);
        delete self;
        return NULL;
    }
    r = rdma_listen(self->listener_id, 128);
    if (r != 0)
    {
        fprintf(stderr, "Failed to listen RDMA-CM: %s (code %d)\n", strerror(errno), errno);
        delete self;
        return NULL;
    }
    return self;
}

void nfs_proxy_t::destroy_rdma()
{
    if (rdma_context)
    {
        delete rdma_context;
        rdma_context = NULL;
    }
}

nfs_rdma_context_t::~nfs_rdma_context_t()
{
    if (listener_id)
    {
        int r = rdma_destroy_id(listener_id);
        if (r != 0)
            fprintf(stderr, "Failed to destroy RDMA-CM ID: %s (code %d)\n", strerror(errno), errno);
        else
            listener_id = NULL;
    }
    if (rdmacm_evch)
    {
        epmgr->tfd->set_fd_handler(rdmacm_evch->fd, false, NULL);
        rdma_destroy_event_channel(rdmacm_evch);
        rdmacm_evch = NULL;
    }
}

void nfs_rdma_context_t::handle_rdmacm_events()
{
    rdma_cm_event *ev = NULL;
    std::vector<nfs_client_t*> stop_clients;
    while (1)
    {
        int r = rdma_get_cm_event(rdmacm_evch, &ev);
        if (r != 0)
        {
            if (errno == EAGAIN || errno == EINTR)
                break;
            fprintf(stderr, "Failed to get RDMA-CM event: %s (code %d)\n", strerror(errno), errno);
            exit(1);
        }
        if (ev->event == RDMA_CM_EVENT_CONNECT_REQUEST)
        {
            rdmacm_accept(ev);
        }
        else if (ev->event == RDMA_CM_EVENT_CONNECT_ERROR ||
            ev->event == RDMA_CM_EVENT_REJECTED ||
            ev->event == RDMA_CM_EVENT_DISCONNECTED ||
            ev->event == RDMA_CM_EVENT_DEVICE_REMOVAL)
        {
            auto event_type_name = ev->event == RDMA_CM_EVENT_CONNECT_ERROR ? "RDMA_CM_EVENT_CONNECT_ERROR" : (
                ev->event == RDMA_CM_EVENT_REJECTED ? "RDMA_CM_EVENT_REJECTED" : (
                ev->event == RDMA_CM_EVENT_DISCONNECTED ? "RDMA_CM_EVENT_DISCONNECTED" : "RDMA_CM_EVENT_DEVICE_REMOVAL"));
            auto conn_it = rdma_connections.find(ev->id);
            if (conn_it == rdma_connections.end())
            {
                fprintf(stderr, "Received %s event for an unknown connection 0x%jx - ignoring\n",
                    event_type_name, (uint64_t)ev->id);
            }
            else
            {
                fprintf(stderr, "Received %s event for connection 0x%jx - closing it\n",
                    event_type_name, (uint64_t)ev->id);
                auto conn = conn_it->second;
                stop_clients.push_back(conn->client);
            }
        }
        else if (ev->event == RDMA_CM_EVENT_ESTABLISHED)
        {
            rdmacm_established(ev);
        }
        else if (ev->event == RDMA_CM_EVENT_ADDR_CHANGE || ev->event == RDMA_CM_EVENT_TIMEWAIT_EXIT)
        {
            // Do nothing
        }
        else
        {
            // Other events are unexpected
            fprintf(stderr, "Unexpected RDMA-CM event type: %d\n", ev->event);
        }
        r = rdma_ack_cm_event(ev);
        if (r != 0)
        {
            fprintf(stderr, "Failed to ack (free) RDMA-CM event: %s (code %d)\n", strerror(errno), errno);
            exit(1);
        }
    }
    // Stop only after flushing all events, otherwise rdma_destroy_id infinitely waits for pthread_cond
    for (auto cli: stop_clients)
    {
        cli->stop();
    }
}

void nfs_rdma_context_t::rdmacm_accept(rdma_cm_event *ev)
{
    nfs_rdma_dev_state_t *conn_dev = nfs_rdma_dev_state_t::create(ev->id, rdma_malloc_round_to, rdma_max_unused_buffers);
    if (!conn_dev)
    {
        rdma_destroy_id(ev->id);
        return;
    }
    ibv_qp_init_attr init_attr = {
        .send_cq = conn_dev->cq,
        .recv_cq = conn_dev->cq,
        .cap     = {
            // each op at each moment takes 1 RDMA_RECV or 1 RDMA_READ or 1 RDMA_WRITE + 1 RDMA_SEND
            .max_send_wr  = max_send_wr,
            .max_recv_wr  = max_iodepth,
            .max_send_sge = 1,
            .max_recv_sge = 1, // we don't need S/G currently
        },
        .qp_type = IBV_QPT_RC,
    };
    int r = rdma_create_qp(ev->id, NULL, &init_attr);
    if (r != 0)
    {
        fprintf(stderr, "Failed to create a queue pair via RDMA-CM: %s (code %d)\n", strerror(errno), errno);
        delete conn_dev;
        rdma_destroy_id(ev->id);
        return;
    }
    assert(ev->id->qp->send_cq == conn_dev->cq);
    assert(ev->id->qp->recv_cq == conn_dev->cq);
    nfs_rdmacm_private private_data = {
        .format_identifier = NFS_RDMACM_PRIVATE_DATA_MAGIC_LE,
        .version = 1,
        .remote_invalidate = 1,
        .max_send_size = (uint8_t)(max_send_size <= 256*1024 ? max_send_size/1024 - 1 : 255),
        .max_recv_size = (uint8_t)(max_recv_size <= 256*1024 ? max_recv_size/1024 - 1 : 255),
    };
    // should be responder_resources, but it's 0 for Linux NFS client
    // so probably RDMA-CM swaps these values in _accept for the "convenience" :)
    int max_rdma_reads = ev->param.conn.initiator_depth;
    // just 16 on ConnectX-4...
    if (!max_rdma_reads || max_rdma_reads > conn_dev->dev_attr.max_qp_rd_atom)
        max_rdma_reads = conn_dev->dev_attr.max_qp_rd_atom;
    if (max_rdma_reads > conn_dev->dev_attr.max_qp_init_rd_atom)
        max_rdma_reads = conn_dev->dev_attr.max_qp_init_rd_atom;
    if (max_rdma_reads > 255)
        max_rdma_reads = 255;
    rdma_conn_param conn_params = {
        .private_data = &private_data,
        .private_data_len = sizeof(private_data),
        .responder_resources = (uint8_t)max_rdma_reads, // min(max_qp_rd_atom of the local device, max_qp_init_rd_atom of the remote device)
        .initiator_depth = (uint8_t)max_rdma_reads, // max_qp_init_rd_atom of the local device
        .rnr_retry_count = 7,
    };
    r = rdma_accept(ev->id, &conn_params);
    if (r != 0)
    {
        fprintf(stderr, "Failed to accept RDMA-CM connection: %s (code %d)\n", strerror(errno), errno);
        rdma_destroy_qp(ev->id);
        delete conn_dev;
        rdma_destroy_id(ev->id);
    }
    else
    {
        auto conn = new nfs_rdma_conn_t();
        conn->ctx = this;
        conn->id = ev->id;
        conn->cur_credit = max_iodepth;
        conn->max_send_size = max_send_size;
        conn->max_recv_size = max_recv_size;
        conn->max_rdma_reads = max_rdma_reads;
        rdma_connections[ev->id] = conn;
        // Handle NFS private_data
        if (ev->param.conn.private_data_len >= sizeof(nfs_rdmacm_private))
        {
            nfs_rdmacm_private *private_data = (nfs_rdmacm_private *)ev->param.conn.private_data;
            if (private_data->format_identifier == NFS_RDMACM_PRIVATE_DATA_MAGIC_LE &&
                private_data->version == 1)
            {
                conn->remote_invalidate = private_data->remote_invalidate;
                conn->remote_max_send_size = (private_data->max_send_size+1) * 1024;
                conn->remote_max_recv_size = (private_data->max_recv_size+1) * 1024;
                if (conn->remote_max_recv_size < conn->max_send_size)
                    conn->max_send_size = conn->remote_max_recv_size;
            }
        }
        auto cli = this->proxy->create_client();
        cli->rdma_conn = conn;
        conn->client = cli;
        conn->conn_dev = conn_dev;
        epmgr->tfd->set_fd_handler(conn_dev->channel->fd, false, [conn](int channel_eventfd, int epoll_events)
        {
            conn->handle_io();
        });
        // run handle_io() once to reset poll state
        conn->handle_io();
        // Post initial receive requests
        conn->post_initial_receives();
    }
}

nfs_rdma_conn_t::~nfs_rdma_conn_t()
{
    assert(!outbox.size());
    assert(!chunk_inbox.size());
    assert(!chunk_read_postponed.size());
    for (auto & b: recv_buffers)
    {
        ibv_dereg_mr(b.mr);
        free(b.buf);
    }
    recv_buffers.clear();
    if (conn_dev)
    {
        ctx->epmgr->tfd->set_fd_handler(conn_dev->channel->fd, false, NULL);
        delete conn_dev;
        conn_dev = NULL;
    }
    if (id)
    {
        ctx->rdma_connections.erase(id);
        if (id->qp)
        {
            rdma_destroy_qp(id);
        }
        rdma_destroy_id(id);
    }
}

void nfs_rdma_context_t::rdmacm_established(rdma_cm_event *ev)
{
    auto conn_it = rdma_connections.find(ev->id);
    if (conn_it == rdma_connections.end())
    {
        fprintf(stderr, "Received RDMA_CM_EVENT_ESTABLISHED event for an unknown connection 0x%jx - ignoring\n", (uint64_t)ev->id);
        return;
    }
    fprintf(stderr, "Received RDMA_CM_EVENT_ESTABLISHED event for connection 0x%jx - connection established\n", (uint64_t)ev->id);
    auto conn = conn_it->second;
    conn->established = true;
}

void nfs_rdma_conn_t::post_initial_receives()
{
    for (int i = 0; i < cur_credit; i++)
    {
        auto b = create_buf(max_recv_size);
        recv_buffers.push_back(b);
        used_buffers[b.buf] = b;
        post_recv(b);
    }
}

nfs_rdma_buf_t nfs_rdma_conn_t::create_buf(size_t len)
{
    nfs_rdma_buf_t b;
    b.buf = malloc_or_die(len);
    b.len = len;
    b.mr = ibv_reg_mr(id->pd, b.buf, len, IBV_ACCESS_LOCAL_WRITE);
    if (!b.mr)
    {
        fprintf(stderr, "Failed to register RDMA memory region: %s\n", strerror(errno));
        exit(1);
    }
    return b;
}

void nfs_rdma_conn_t::post_recv(nfs_rdma_buf_t b)
{
    if (client->stopped)
    {
        return;
    }
    ibv_sge sge = {
        .addr = (uintptr_t)b.buf,
        .length = (uint32_t)b.len,
        .lkey = b.mr->lkey,
    };
    ibv_recv_wr *bad_wr = NULL;
    ibv_recv_wr wr = {
        .wr_id   = 1, // 1 is any read, 2 is any write :)
        .sg_list = &sge,
        .num_sge = 1,
    };
    int err = ibv_post_recv(id->qp, &wr, &bad_wr);
    if (err || bad_wr)
    {
        fprintf(stderr, "RDMA receive failed: %s\n", strerror(err));
        exit(1);
    }
}

void nfs_rdma_conn_t::rdma_encode_header(XDR *xdrs, rpc_op_t *rop, bool nomsg)
{
    rdma_msg outrmsg = {
        .rdma_xid = rop->in_rdma_msg.rdma_xid,
        .rdma_vers = rop->in_rdma_msg.rdma_vers,
        .rdma_credit = cur_credit,
        .rdma_body = {
            .proc = rop->rdma_error ? RDMA_ERROR : (nomsg ? RDMA_NOMSG : RDMA_MSG),
        },
    };
    if (rop->rdma_error)
    {
        outrmsg.rdma_body.rdma_error.err = rop->rdma_error;
        if (rop->rdma_error == ERR_VERS)
            outrmsg.rdma_body.rdma_error.range = (rpc_rdma_errvers){ 1, 1 };
    }
    else
    {
        // Copy chunks... it's a real shit
        outrmsg.rdma_body.rdma_msg = (rpc_rdma_header){
            .rdma_writes = rop->in_rdma_msg.rdma_body.rdma_msg.rdma_writes,
            .rdma_reply = rop->in_rdma_msg.rdma_body.rdma_msg.rdma_reply,
        };
    }
    int r = xdr_encode(xdrs, (xdrproc_t)xdr_rdma_msg, &outrmsg);
    assert(r);
}

void nfs_client_t::rdma_queue_reply(rpc_op_t *rop)
{
    rdma_conn->outbox.push_back(rop);
    rdma_conn->post_send();
}

void nfs_rdma_conn_t::post_send()
{
    while (chunk_read_postponed.size() > 0)
    {
        int posted = post_chunk_reads(chunk_read_postponed[0], false);
        if (posted)
            chunk_read_postponed.erase(chunk_read_postponed.begin(), chunk_read_postponed.begin()+1);
        else
            break;
    }
    while (outbox.size() > outbox_pos)
    {
        auto rop = outbox[outbox_pos];
        if (rop->buffer)
        {
            reuse_buffer(rop->buffer);
            rop->buffer = NULL;
        }
        XDR *hdr_xdr = ctx->proxy->get_xdr();
send_again:
        rdma_encode_header(hdr_xdr, rop, false);
        size_t hdr_size = xdr_encode_get_size(hdr_xdr);
        iovec *chunk_iov = NULL;
        iovec *iov_list = NULL;
        unsigned iov_count = 0;
        if (!rop->rdma_error)
        {
            xdr_encode_finish(rop->xdrs, &iov_list, &iov_count);
            assert(iov_count > 0);
            // READ3resok and READLINK3resok - extract last byte buffer from iovecs and send it in a "write chunk"
            if (rop->in_rdma_msg.rdma_body.rdma_msg.rdma_writes &&
                rop->in_msg.body.cbody.prog == NFS_PROGRAM &&
                (rop->in_msg.body.cbody.proc == NFS3_READ && ((READ3res*)rop->reply)->status == NFS3_OK ||
                rop->in_msg.body.cbody.proc == NFS3_READLINK && ((READLINK3res*)rop->reply)->status == NFS3_OK))
            {
                assert(iov_count > 1);
                iov_count--;
                chunk_iov = &iov_list[iov_count];
                auto expected_size = (rop->in_msg.body.cbody.proc == NFS3_READ
                    ? ((READ3res*)rop->reply)->resok.count
                    : ((READLINK3res*)rop->reply)->resok.data.size);
                assert(chunk_iov->iov_len == expected_size);
            }
        }
        size_t msg_size = 0;
        for (unsigned i = 0; i < iov_count; i++)
        {
            msg_size += iov_list[i].iov_len;
        }
        // Estimate reply WR count, create WR and SGE arrays
        xdr_write_chunk *reply_chunk = rop->in_rdma_msg.rdma_body.rdma_msg.rdma_reply;
        int reply_chunk_wr_count = (reply_chunk ? reply_chunk->target.target_len : 0);
        uint32_t wr_count = 1 + (chunk_iov ? 1 : 0) + (reply_chunk ? reply_chunk_wr_count : 0);
        if (wr_count > ctx->max_send_wr)
        {
            fprintf(stderr, "Reply fragmentation (%u) exceeds max_send_wr (%u), sending ERR_CHUNK\n", wr_count, ctx->max_send_wr);
chunk_error:
            xdr_reset(hdr_xdr);
            rop->rdma_error = ERR_CHUNK;
            goto send_again;
        }
        if (msg_size+hdr_size > max_send_size && !reply_chunk)
        {
            fprintf(stderr, "RPC message size (%zu) exceeds client's max_send_size (%u)"
                " and reply chunk is not provided, sending ERR_CHUNK\n", msg_size+hdr_size, max_send_size);
            goto chunk_error;
        }
        if (chunk_iov && chunk_iov->iov_len > rop->in_rdma_msg.rdma_body.rdma_msg.rdma_writes->entry.target.target_val[0].length)
        {
            fprintf(stderr, "%s write chunk size (%u) is smaller than read size (%zu), sending ERR_CHUNK\n",
                rop->in_msg.body.cbody.proc == NFS3_READ ? "READ3" : "READLINK3",
                rop->in_rdma_msg.rdma_body.rdma_msg.rdma_writes->entry.target.target_val[0].length,
                chunk_iov->iov_len);
            goto chunk_error;
        }
        if (cur_send + wr_count > ctx->max_send_wr)
        {
            // Retry later
            ctx->proxy->free_xdr(hdr_xdr);
            return;
        }
        // Check reply chunk size and set actual reply segment sizes
        bool reencode = false;
        if (reply_chunk)
        {
            size_t reply_chunk_len = 0;
            size_t left = msg_size;
            for (uint32_t i = 0; i < reply_chunk->target.target_len; i++)
            {
                reply_chunk_len += reply_chunk->target.target_val[i].length;
                if (reply_chunk->target.target_val[i].length > left)
                    reply_chunk->target.target_val[i].length = left;
                left -= reply_chunk->target.target_val[i].length;
            }
            if (left > 0)
            {
                // Message doesn't fit even in the reply chunk
                fprintf(stderr, "RPC message payload size (%zu) exceeds reply chunk size (%zu), sending ERR_CHUNK\n",
                    msg_size, reply_chunk_len);
                goto chunk_error;
            }
            reencode = true;
        }
        if (chunk_iov && chunk_iov->iov_len < rop->in_rdma_msg.rdma_body.rdma_msg.rdma_writes->entry.target.target_val[0].length)
        {
            // Set actual write chunk size
            reencode = true;
            rop->in_rdma_msg.rdma_body.rdma_msg.rdma_writes->entry.target.target_val[0].length = chunk_iov->iov_len;
        }
        if (reencode)
        {
            // ...and re-encode the header :-( shitty protocol
            xdr_reset(hdr_xdr);
            rdma_encode_header(hdr_xdr, rop, reply_chunk != NULL);
            hdr_size = xdr_encode_get_size(hdr_xdr);
        }
        ibv_sge sges[wr_count];
        ibv_send_wr wrs[wr_count];
        int wr_pos = 0;
        // Use a buffer from rdma_malloc for the reply
        assert(!rop->buffer);
        rop->buffer = rdma_malloc_alloc(conn_dev->alloc, hdr_size+msg_size);
        auto buf_lkey = rdma_malloc_get_lkey(conn_dev->alloc, rop->buffer);
        size_t pos = 0;
        {
            // Copy and free the RDMA-RPC header
            iovec *hdr_iov_list = NULL;
            unsigned hdr_iov_count = 0;
            xdr_encode_finish(hdr_xdr, &hdr_iov_list, &hdr_iov_count);
            assert(hdr_iov_count > 0);
            for (unsigned i = 0; i < hdr_iov_count; i++)
            {
                memcpy((uint8_t*)rop->buffer + pos, hdr_iov_list[i].iov_base, hdr_iov_list[i].iov_len);
                pos += hdr_iov_list[i].iov_len;
            }
            assert(pos == hdr_size);
            ctx->proxy->free_xdr(hdr_xdr);
        }
        for (unsigned i = 0; i < iov_count; i++)
        {
            memcpy((uint8_t*)rop->buffer + pos, iov_list[i].iov_base, iov_list[i].iov_len);
            pos += iov_list[i].iov_len;
        }
        // Include header size in msg_size now and then
        msg_size += hdr_size;
        assert(msg_size == pos);
        // Check if we have to use the reply chunk
        if (reply_chunk)
        {
            size_t pos = hdr_size;
            for (uint32_t i = 0; i < reply_chunk->target.target_len && pos < msg_size; i++)
            {
                uint32_t len = (reply_chunk->target.target_val[i].length < msg_size-pos
                    ? reply_chunk->target.target_val[i].length : msg_size-pos);
                sges[wr_pos] = {
                    .addr = (uintptr_t)((uint8_t*)rop->buffer + pos),
                    .length = len,
                    .lkey = buf_lkey,
                };
                wrs[wr_pos] = (ibv_send_wr){
                    .wr_id = 4, // 4 is chunk write
                    .opcode = IBV_WR_RDMA_WRITE,
                    .wr = {
                        .rdma = {
                            .remote_addr = reply_chunk->target.target_val[i].offset,
                            .rkey = reply_chunk->target.target_val[i].handle,
                        },
                    },
                };
                wr_pos++;
                pos += len;
            }
        }
        // Check if we have a DDP chunk
        xdr_rdma_segment *wr_chunk = NULL;
        if (chunk_iov != NULL)
        {
            wr_chunk = rop->in_rdma_msg.rdma_body.rdma_msg.rdma_writes->entry.target.target_val;
            sges[wr_pos] = {
                .addr = (uintptr_t)chunk_iov->iov_base,
                .length = (uint32_t)chunk_iov->iov_len,
                .lkey = rdma_malloc_get_lkey(conn_dev->alloc, chunk_iov->iov_base),
            };
            wrs[wr_pos] = (ibv_send_wr){
                .wr_id = 4, // 4 is chunk write
                .opcode = IBV_WR_RDMA_WRITE,
                .wr = {
                    .rdma = {
                        .remote_addr = wr_chunk->offset,
                        .rkey = wr_chunk->handle,
                    },
                },
            };
            wr_pos++;
        }
        // Add the normal send
        sges[wr_pos] = {
            .addr = (uintptr_t)rop->buffer,
            .length = (uint32_t)(reply_chunk ? hdr_size : msg_size),
            .lkey = buf_lkey,
        };
        wrs[wr_pos] = (ibv_send_wr){
            .wr_id = 2, // 2 is send
            .opcode = remote_invalidate && !reply_chunk && wr_chunk ? IBV_WR_SEND_WITH_INV : IBV_WR_SEND,
            .send_flags = IBV_SEND_SIGNALED,
            .invalidate_rkey = remote_invalidate && !reply_chunk && wr_chunk ? wr_chunk->handle : 0,
        };
        wr_pos++;
        // Send it all
        for (int i = 0; i < wr_pos; i++)
        {
            wrs[i].next = &wrs[i+1];
            wrs[i].sg_list = &sges[i];
            wrs[i].num_sge = 1;
        }
        wrs[wr_pos-1].next = NULL;
        ibv_send_wr *bad_wr = NULL;
        int err = ibv_post_send(id->qp, &wrs[0], &bad_wr);
        if (err || bad_wr)
        {
            fprintf(stderr, "Posting RDMA send failed: %s\n", strerror(err));
            exit(1);
        }
        cur_send += wr_pos;
        outbox_wrs.push_back(wr_pos);
        outbox_pos++;
    }
}

#define RDMA_EVENTS_AT_ONCE 32

void nfs_rdma_conn_t::handle_io()
{
    auto conn = this;
    // Request next notification
    ibv_cq *ev_cq;
    void *ev_ctx;
    // This is inefficient as it calls read()... (but there is no other way)
    if (ibv_get_cq_event(conn_dev->channel, &ev_cq, &ev_ctx) == 0)
    {
        ibv_ack_cq_events(conn_dev->cq, 1);
    }
    if (ibv_req_notify_cq(conn_dev->cq, 0) != 0)
    {
        fprintf(stderr, "Failed to request RDMA completion notification, exiting\n");
        exit(1);
    }
    ibv_wc wc[RDMA_EVENTS_AT_ONCE];
    int event_count;
    do
    {
        event_count = ibv_poll_cq(conn_dev->cq, RDMA_EVENTS_AT_ONCE, wc);
        for (int i = 0; i < event_count; i++)
        {
            if (wc[i].status != IBV_WC_SUCCESS)
            {
                fprintf(stderr, "RDMA work request failed for queue %d with status: %s, stopping client\n", wc[i].qp_num, ibv_wc_status_str(wc[i].status));
                conn->client->stop();
                // but continue to handle events to purge the queue
            }
            if (wc[i].wr_id == 1)
            {
                // 1 = receive
                auto b = conn->recv_buffers[conn->next_recv_buf];
                // Due to the credit-based flow control in RPC-RDMA, we can just remove that buffer and reuse it later
                conn->recv_buffers.erase(conn->recv_buffers.begin()+conn->next_recv_buf, conn->recv_buffers.begin()+conn->next_recv_buf+1);
                if (conn->cur_credit > 0 && !conn->client->stopped)
                {
                    // Increase client refcount while the RPC call is being processed
                    conn->client->refs++;
                    conn->cur_credit--;
                    conn->handle_recv(b.buf, wc[i].byte_len);
                }
                else
                {
                    fprintf(stderr, "Warning: NFS client credit exceeded for queue %d, stopping client\n", wc[i].qp_num);
                    conn->client->stop();
                }
            }
            else if (wc[i].wr_id == 2)
            {
                // 2 = send
                auto rop = conn->outbox[0];
                conn->cur_send -= conn->outbox_wrs[0];
                conn->outbox.erase(conn->outbox.begin(), conn->outbox.begin()+1);
                conn->outbox_wrs.erase(conn->outbox_wrs.begin(), conn->outbox_wrs.begin()+1);
                conn->outbox_pos--;
                // Retry send for corner cases of exceeded max_send_wr
                conn->post_send();
                // Free rpc_op
                conn->free_rdma_rpc_op(rop);
            }
            else if (wc[i].wr_id == 3)
            {
                // 3 = chunk read
                auto rop = conn->chunk_inbox[0];
                conn->chunk_inbox.erase(conn->chunk_inbox.begin(), conn->chunk_inbox.begin()+1);
                size_t read_chunk_count = 0;
                for (auto cur = rop->in_rdma_msg.rdma_body.rdma_msg.rdma_reads; cur; cur = cur->next)
                    read_chunk_count++;
                conn->cur_send -= read_chunk_count;
                conn->cur_rdma_reads -= read_chunk_count;
                conn->client->handle_rpc_op(rop);
                // Retry send for corner cases of exceeded max_send_wr
                conn->post_send();
            }
            else
            {
                fprintf(stderr, "BUG: unknown RDMA work completion with wr_id=%ju\n", wc[i].wr_id);
            }
        }
    } while (event_count > 0);
}

void nfs_rdma_conn_t::free_rdma_rpc_op(rpc_op_t *rop)
{
    if (rop->buffer)
    {
        rdma_malloc_free(conn_dev->alloc, rop->buffer);
        rop->buffer = NULL;
    }
    auto rdma_chunk = xdr_get_rdma_chunk(rop->xdrs);
    if (rdma_chunk)
    {
        rdma_malloc_free(conn_dev->alloc, rdma_chunk);
        xdr_set_rdma_chunk(rop->xdrs, NULL);
    }
    ctx->proxy->free_xdr(rop->xdrs);
    free(rop);
    cur_credit++;
    client->deref();
}

void nfs_rdma_conn_t::reuse_buffer(void *buf)
{
    auto ub = used_buffers.at(buf);
    recv_buffers.push_back(ub);
    post_recv(ub);
}

// returns false if handling is done, returns true if handling is continued asynchronously
bool nfs_rdma_conn_t::handle_recv(void *buf, size_t len)
{
    XDR *xdrs = ctx->proxy->get_xdr();
    xdr_set_rdma(xdrs);
    // Decode the RDMA-RPC header
    rdma_msg rmsg;
    if (!xdr_decode(xdrs, buf, len, (xdrproc_t)xdr_rdma_msg, &rmsg))
    {
        // Invalid message, ignore it
        fprintf(stderr, "Invalid RDMA-RPC header on connection 0x%jx, ignoring message\n", (uint64_t)this);
ignore_msg:
        reuse_buffer(buf);
        ctx->proxy->free_xdr(xdrs);
        client->deref();
        return 0;
    }
    if (rmsg.rdma_vers != 1 || rmsg.rdma_body.proc != RDMA_MSG)
    {
        // Bad RDMA-RPC version or message type
        fprintf(
            stderr, "Unsupported RDMA-RPC version (%d) or message type (%d), sending %s\n",
            rmsg.rdma_vers, rmsg.rdma_body.proc,
            rmsg.rdma_vers != 1 ? "ERR_VERS" : "ERR_CHUNK"
        );
        rpc_op_t *rop = (rpc_op_t*)malloc_or_die(sizeof(rpc_op_t));
        *rop = (rpc_op_t){
            .client = this,
            .xdrs = xdrs,
            .in_msg = {
                .xid = rmsg.rdma_xid,
            },
            .in_rdma_msg = rmsg,
            .rdma_error = rmsg.rdma_vers != 1 ? ERR_VERS : ERR_CHUNK,
        };
        rpc_queue_reply(rop);
        // Incoming buffer isn't needed to handle request, so return 0
        return 0;
    }
    rpc_msg inmsg;
    if (!xdr_rpc_msg(xdrs, &inmsg))
    {
        // Invalid message, ignore it
        fprintf(stderr, "Invalid RDMA-RPC body on connection 0x%jx, ignoring message\n", (uint64_t)this);
        goto ignore_msg;
    }
    if (inmsg.xid != rmsg.rdma_xid)
    {
        fprintf(stderr, "RPC XID (%u) and RDMA XID (%u) mismatch on connection 0x%jx, ignoring message\n", inmsg.xid, rmsg.rdma_xid, (uint64_t)this);
        goto ignore_msg;
    }
    if (inmsg.body.dir != RPC_CALL)
    {
        fprintf(stderr, "Non-RPC_CALL message direction (%d) on connection 0x%jx, ignoring message\n", inmsg.body.dir, (uint64_t)this);
        goto ignore_msg;
    }
    rpc_op_t *rop = client->create_rpc_op(xdrs, buf, &inmsg, &rmsg);
    if (!rop)
    {
        // No such procedure
        return 0;
    }
    // Read chunks may only be provided for WRITE3 and SYMLINK3
    if (inmsg.body.cbody.prog == NFS_PROGRAM &&
        rmsg.rdma_body.rdma_msg.rdma_reads &&
        inmsg.body.cbody.proc != NFS3_WRITE && inmsg.body.cbody.proc != NFS3_SYMLINK)
    {
        fprintf(stderr, "Read chunk(s) are provided for non-WRITE or SYMLINK operation (procedure %u), sending ERR_CHUNK\n", inmsg.body.cbody.proc);
        rop->rdma_error = ERR_CHUNK;
        rpc_queue_reply(rop);
        return 0;
    }
    // Write chunk may be provided only for READ3 and READLINK3
    if (inmsg.body.cbody.prog == NFS_PROGRAM &&
        rmsg.rdma_body.rdma_msg.rdma_writes &&
        inmsg.body.cbody.proc != NFS3_READ && inmsg.body.cbody.proc != NFS3_READLINK)
    {
        fprintf(stderr, "Write chunk(s) are provided for non-READ or READLINK operation (procedure %u), sending ERR_CHUNK\n", inmsg.body.cbody.proc);
        rop->rdma_error = ERR_CHUNK;
        rpc_queue_reply(rop);
        return 0;
    }
    // Only 1 write chunk is supported
    if (inmsg.body.cbody.prog == NFS_PROGRAM &&
        rmsg.rdma_body.rdma_msg.rdma_writes && (
        rmsg.rdma_body.rdma_msg.rdma_writes->next ||
        rmsg.rdma_body.rdma_msg.rdma_writes->entry.target.target_len != 1))
    {
        int n = 0, seg = 0;
        for (auto cur = rmsg.rdma_body.rdma_msg.rdma_writes; cur; cur = cur->next)
        {
            n++;
            seg += cur->entry.target.target_len;
        }
        fprintf(stderr, "Only 1 write chunk with 1 segment may be provided, but we got %d chunks with %d segments, sending ERR_CHUNK\n", n, seg);
        rop->rdma_error = ERR_CHUNK;
        rpc_queue_reply(rop);
        return 0;
    }
    // Process read chunk(s)
    if (rmsg.rdma_body.rdma_msg.rdma_reads)
    {
        int r = post_chunk_reads(rop, true);
        return (r >= 0 ? 1 : 0);
    }
    return client->handle_rpc_op(rop);
}

int nfs_rdma_conn_t::post_chunk_reads(rpc_op_t *rop, bool push)
{
    rop->referenced = 1;
    size_t read_chunk_size = 0;
    size_t read_chunk_count = 0;
    for (auto cur = rop->in_rdma_msg.rdma_body.rdma_msg.rdma_reads; cur; cur = cur->next)
    {
        read_chunk_size += cur->entry.target.length;
        read_chunk_count++;
    }
    if (read_chunk_count > max_rdma_reads)
    {
        fprintf(stderr, "Read chunk count (%zu) exceeds max_rdma_reads (%d), sending ERR_CHUNK\n", read_chunk_count, max_rdma_reads);
        rop->rdma_error = ERR_CHUNK;
        rpc_queue_reply(rop);
        return -1;
    }
    if (cur_send+read_chunk_count > ctx->max_send_wr ||
        cur_rdma_reads+read_chunk_count > max_rdma_reads)
    {
        // Send it later
        if (push)
            chunk_read_postponed.push_back(rop);
        return 0;
    }
    void *buf = rdma_malloc_alloc(conn_dev->alloc, read_chunk_size);
    auto buf_lkey = rdma_malloc_get_lkey(conn_dev->alloc, buf);
    ibv_sge chunk_sge[read_chunk_count];
    ibv_send_wr chunk_wr[read_chunk_count];
    size_t i = 0;
    size_t pos = 0;
    for (auto cur = rop->in_rdma_msg.rdma_body.rdma_msg.rdma_reads; cur; cur = cur->next)
    {
        chunk_sge[i] = {
            .addr = (uintptr_t)((uint8_t*)buf + pos),
            .length = cur->entry.target.length,
            .lkey = buf_lkey,
        };
        chunk_wr[i] = (ibv_send_wr){
            .wr_id = 3, // 3 is chunk read
            .next = (i == read_chunk_count-1 ? NULL : &chunk_wr[i+1]),
            .sg_list = &chunk_sge[i],
            .num_sge = 1,
            .opcode = IBV_WR_RDMA_READ,
            .send_flags = (unsigned)(i == read_chunk_count-1 ? IBV_SEND_SIGNALED : 0),
            .wr = {
                .rdma = {
                    .remote_addr = cur->entry.target.offset,
                    .rkey = cur->entry.target.handle,
                },
            },
        };
        pos += cur->entry.target.length;
        i++;
    }
    assert(i == read_chunk_count);
    assert(pos == read_chunk_size);
    ibv_send_wr *bad_wr = NULL;
    int err = ibv_post_send(id->qp, &chunk_wr[0], &bad_wr);
    if (err || bad_wr)
    {
        fprintf(stderr, "Posting RDMA read failed: %s\n", strerror(err));
        exit(1);
    }
    xdr_set_rdma_chunk(rop->xdrs, buf);
    chunk_inbox.push_back(rop);
    cur_send += read_chunk_count;
    cur_rdma_reads += read_chunk_count;
    return 1;
}

void *nfs_client_t::rdma_malloc(size_t size)
{
    return rdma_malloc_alloc(rdma_conn->conn_dev->alloc, size);
}

void nfs_client_t::rdma_free(void *buf)
{
    rdma_malloc_free(rdma_conn->conn_dev->alloc, buf);
}

void nfs_client_t::destroy_rdma_conn()
{
    if (rdma_conn)
    {
        delete rdma_conn;
        rdma_conn = NULL;
    }
}

#endif
