/*
 * Copyright (c) Likewise Software.  All rights Reserved.
 *
 * This library is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 2.1 of the license, or (at
 * your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser
 * General Public License for more details.  You should have received a copy
 * of the GNU Lesser General Public License along with this program.  If
 * not, see <http://www.gnu.org/licenses/>.
 *
 * LIKEWISE SOFTWARE MAKES THIS SOFTWARE AVAILABLE UNDER OTHER LICENSING
 * TERMS AS WELL.  IF YOU HAVE ENTERED INTO A SEPARATE LICENSE AGREEMENT
 * WITH LIKEWISE SOFTWARE, THEN YOU MAY ELECT TO USE THE SOFTWARE UNDER THE
 * TERMS OF THAT SOFTWARE LICENSE AGREEMENT INSTEAD OF THE TERMS OF THE GNU
 * LESSER GENERAL PUBLIC LICENSE, NOTWITHSTANDING THE ABOVE NOTICE.  IF YOU
 * HAVE QUESTIONS, OR WISH TO REQUEST A COPY OF THE ALTERNATE LICENSING
 * TERMS OFFERED BY LIKEWISE SOFTWARE, PLEASE CONTACT LIKEWISE SOFTWARE AT
 * license@likewisesoftware.com
 */

/*
 * Module Name:
 *
 *        peer.c
 *
 * Abstract:
 *
 *        Multi-threaded peer API
 *
 * Authors: Brian Koropoff (bkoropoff@likewisesoftware.com)
 *
 */

#include <config.h>
#include "peer-private.h"
#include "assoc-private.h"
#include "session-private.h"
#include "connection-private.h"
#include "buffer-private.h"
#include "security-private.h"

#include <errno.h>
#include <fcntl.h>
#include <sys/un.h>
#include <sys/socket.h>
#include <sys/stat.h>

static
void
lwmsg_peer_assoc_session_run(
    LWMsgTask* _task,
    void* data,
    LWMsgTaskTrigger trigger,
    LWMsgTaskTrigger* next_trigger,
    LWMsgTaskTime* next_timeout
    );

static
void
lwmsg_peer_assoc_endpoint_run_listen(
    LWMsgTask* _task,
    void* data,
    LWMsgTaskTrigger trigger,
    LWMsgTaskTrigger* next_trigger,
    LWMsgTaskTime* next_timeout
    );

static
void
lwmsg_peer_assoc_session_update_blocked(
    PeerAssocSession* session
    )
{
    switch (lwmsg_assoc_get_state(session->assoc))
    {
    case LWMSG_ASSOC_STATE_BLOCKED_SEND:
        session->send_blocked = LWMSG_TRUE;
        session->recv_blocked = LWMSG_FALSE;
        break;
    case LWMSG_ASSOC_STATE_BLOCKED_RECV:
        session->recv_blocked = LWMSG_TRUE;
        session->send_blocked = LWMSG_FALSE;
        break;
    case LWMSG_ASSOC_STATE_BLOCKED_SEND_RECV:
        session->send_blocked = session->recv_blocked = LWMSG_TRUE;
        break;
    default:
        session->send_blocked = session->recv_blocked = LWMSG_FALSE;
        break;
    }
}

static
void
lwmsg_peer_assoc_session_delete(
    PeerAssocSession* session
    )
{
    LWMsgHashIter iter = {0};
    PeerAssocCall* call = NULL;
    LWMsgRing* ring = NULL;
    LWMsgRing* next = NULL;

    lwmsg_hash_iter_begin(&session->incoming_calls, &iter);
    while ((call = lwmsg_hash_iter_next(&session->incoming_calls, &iter)))
    {
        lwmsg_hash_remove_entry(&session->incoming_calls, call);
        lwmsg_peer_assoc_call_release(call);
    }
    lwmsg_hash_iter_end(&session->incoming_calls, &iter);

    lwmsg_hash_destroy(&session->incoming_calls);

    lwmsg_hash_iter_begin(&session->outgoing_calls, &iter);
    while ((call = lwmsg_hash_iter_next(&session->outgoing_calls, &iter)))
    {
        lwmsg_hash_remove_entry(&session->outgoing_calls, call);
        lwmsg_peer_assoc_call_release(call);
    }
    lwmsg_hash_iter_end(&session->outgoing_calls, &iter);

    lwmsg_hash_destroy(&session->outgoing_calls);

    for (ring = session->active_incoming_calls.next;
         ring != &session->active_incoming_calls;
         ring = next)
    {
        next = ring->next;
        call = LWMSG_OBJECT_FROM_MEMBER(ring, PeerAssocCall, queue_ring);
        lwmsg_peer_assoc_call_release(call);
    }

    for (ring = session->active_outgoing_calls.next;
         ring != &session->active_outgoing_calls;
         ring = next)
    {
        next = ring->next;
        call = LWMSG_OBJECT_FROM_MEMBER(ring, PeerAssocCall, queue_ring);
        lwmsg_peer_assoc_call_release(call);
    }

    if (session->assoc)
    {
        lwmsg_assoc_destroy_message(session->assoc, &session->incoming_message);
        if (session->destroy_outgoing)
        {
            lwmsg_assoc_destroy_message(session->assoc, &session->outgoing_message);
        }
        lwmsg_assoc_delete(session->assoc);
    }

    if (session->event_task)
    {
        lwmsg_task_release(session->event_task);
    }

    if (session->session)
    {
        if (!session->session->is_outgoing)
        {
            lwmsg_peer_release_client_slot(session->session->peer);
        }

        lwmsg_peer_session_release(session->session);
    }

    free(session);
}

void
lwmsg_peer_assoc_session_release(
    PeerAssocSession* session
    )
{
    int32_t refs = 0;

    LOCK_MUTEX(&session->session->lock);
    refs = --session->refs;
    UNLOCK_MUTEX(&session->session->lock);

    if (refs == 0)
    {
        lwmsg_peer_assoc_session_delete(session);
    }
}

static
void*
lwmsg_peer_assoc_call_get_key(
    const void* entry
    )
{
    return &((PeerAssocCall*) entry)->cookie;
}

static
size_t
lwmsg_peer_assoc_call_digest(
    const void* key
    )
{
    return (size_t) *(LWMsgCookie*) key;
}

static
LWMsgBool
lwmsg_peer_assoc_call_equal(
    const void* key1,
    const void* key2
    )
{
    return *((const LWMsgCookie*) key1) == *((const LWMsgCookie*) key2);
}

static LWMsgStatus
lwmsg_peer_assoc_session_new(
    PeerSession* session,
    PeerAssocSessionState type,
    PeerAssocSession** assoc_session
    )
{
    LWMsgStatus status = LWMSG_STATUS_SUCCESS;
    PeerAssocSession* my_session = NULL;

    BAIL_ON_ERROR(status = LWMSG_ALLOC(&my_session));

    my_session->session = session;
    my_session->state = type;
    my_session->refs = 1;

    lwmsg_ring_init(&my_session->active_incoming_calls);
    lwmsg_ring_init(&my_session->active_outgoing_calls);

    BAIL_ON_ERROR(status = lwmsg_hash_init(
                      &my_session->incoming_calls,
                      31,
                      lwmsg_peer_assoc_call_get_key,
                      lwmsg_peer_assoc_call_digest,
                      lwmsg_peer_assoc_call_equal,
                      offsetof(PeerAssocCall, hash_ring)));

    BAIL_ON_ERROR(status = lwmsg_hash_init(
                      &my_session->outgoing_calls,
                      31,
                      lwmsg_peer_assoc_call_get_key,
                      lwmsg_peer_assoc_call_digest,
                      lwmsg_peer_assoc_call_equal,
                      offsetof(PeerAssocCall, hash_ring)));

    lwmsg_message_init(&my_session->incoming_message);
    lwmsg_message_init(&my_session->outgoing_message);

    *assoc_session = my_session;

error:

    if (status && my_session)
    {
        my_session->session = NULL;
        lwmsg_peer_assoc_session_delete(my_session);
    }

    return status;
}


static LWMsgStatus
lwmsg_peer_assoc_endpoint_create_local_socket(
    PeerAssocEndpoint* endpoint
    )
{
    LWMsgStatus status = LWMSG_STATUS_SUCCESS;
    int sock = -1;
    struct sockaddr_un sockaddr;

    sock = socket(AF_UNIX, SOCK_STREAM, 0);

    if (sock == -1)
    {
        BAIL_ON_ERROR(status = lwmsg_status_map_errno(errno));
    }

    BAIL_ON_ERROR(status = lwmsg_set_close_on_exec(sock));

    sockaddr.sun_len = strlen(endpoint->endpoint) + 1;
    sockaddr.sun_family = AF_UNIX;

    if (sockaddr.sun_len > sizeof(sockaddr.sun_path))
    {
        BAIL_ON_ERROR(status = LWMSG_STATUS_INVALID_PARAMETER);
    }

    sockaddr.sun_len += offsetof(struct sockaddr_un, sun_path);

    strcpy(sockaddr.sun_path, endpoint->endpoint);
    unlink(sockaddr.sun_path);

    if (bind(sock, (struct sockaddr*) &sockaddr, sizeof(sockaddr)) == -1)
    {
        BAIL_ON_ERROR(status = lwmsg_status_map_errno(errno));
    }

    // ignore errors
    chmod(sockaddr.sun_path, endpoint->perms);

    endpoint->fd = sock;

done:

    return status;

error:

    if (sock != -1)
    {
        close(sock);
    }

    goto done;
}

static
LWMsgStatus
lwmsg_peer_assoc_endpoint_setup_listen(
    PeerAssocEndpoint* endpoint
    )
{
    LWMsgStatus status = LWMSG_STATUS_SUCCESS;
    long opts = 0;

    /* Create and bind socket if needed */
    if (endpoint->fd == -1)
    {
        BAIL_ON_ERROR(status = lwmsg_peer_assoc_endpoint_create_local_socket(endpoint));
    }

    /* Get socket flags */
    if ((opts = fcntl(endpoint->fd, F_GETFL, 0)) < 0)
    {
        BAIL_ON_ERROR(status = lwmsg_status_map_errno(errno));
    }

    /* Set non-blocking flag */
    opts |= O_NONBLOCK;

    /* Set socket flags */
    if (fcntl(endpoint->fd, F_SETFL, opts) < 0)
    {
        BAIL_ON_ERROR(status = lwmsg_status_map_errno(errno));
    }

    if (listen(endpoint->fd, endpoint->peer->max_backlog))
    {
        BAIL_ON_ERROR(status = lwmsg_status_map_errno(errno));
    }

error:

    return status;
}

LWMsgStatus
lwmsg_peer_assoc_endpoint_new(
    LWMsgPeer* peer,
    LWMsgEndpointType type,
    const char* endpoint,
    mode_t perms,
    int fd,
    PeerAssocEndpoint** assoc_endpoint
    )
{
    LWMsgStatus status = LWMSG_STATUS_SUCCESS;
    PeerAssocEndpoint* my_endpoint = NULL;

    BAIL_ON_ERROR(status = LWMSG_ALLOC(&my_endpoint));

    BAIL_ON_ERROR(status = lwmsg_task_new(
                      peer->task_manager,
                      peer->listen_tasks,
                      lwmsg_peer_assoc_endpoint_run_listen,
                      my_endpoint,
                      &my_endpoint->event_task));

    my_endpoint->peer = peer;

    if (endpoint)
    {
        my_endpoint->endpoint = strdup(endpoint);

        if (!my_endpoint->endpoint)
        {
            BAIL_ON_ERROR(status = LWMSG_STATUS_MEMORY);
        }
    }

    my_endpoint->fd = fd;
    my_endpoint->type = type;
    my_endpoint->perms = perms;

    BAIL_ON_ERROR(status = lwmsg_peer_assoc_endpoint_setup_listen(my_endpoint));

    *assoc_endpoint = my_endpoint;

error:

    if (status && my_endpoint)
    {
        lwmsg_peer_assoc_endpoint_delete(my_endpoint);
    }

    return status;
}

LWMsgStatus
lwmsg_peer_assoc_session_new_accept(
    PeerSession* session,
    LWMsgAssoc* assoc,
    PeerAssocSession** assoc_session
    )
{
    LWMsgStatus status = LWMSG_STATUS_SUCCESS;
    PeerAssocSession* my_session = NULL;

    BAIL_ON_ERROR(status = lwmsg_peer_assoc_session_new(session, PEER_TASK_BEGIN_ACCEPT, &my_session));

    my_session->assoc = assoc;

    status = lwmsg_task_new(
        session->peer->task_manager,
        session->peer->listen_tasks,
        lwmsg_peer_assoc_session_run,
        my_session,
        &my_session->event_task);
    if (status != LWMSG_STATUS_SUCCESS)
    {
        my_session->assoc = NULL;
    }
    BAIL_ON_ERROR(status);

    *assoc_session = my_session;

done:

    return status;

error:

    if (my_session)
    {
        my_session->session = NULL;
        lwmsg_peer_assoc_session_delete(my_session);
    }

    goto done;
}

LWMsgStatus
lwmsg_peer_assoc_session_new_connect(
    PeerSession* session,
    LWMsgAssoc* assoc,
    PeerAssocSession** assoc_session
    )
{
    LWMsgStatus status = LWMSG_STATUS_SUCCESS;
    PeerAssocSession* my_session = NULL;

    BAIL_ON_ERROR(status = lwmsg_peer_assoc_session_new(session, PEER_TASK_BEGIN_CONNECT, &my_session));

    my_session->assoc = assoc;
    my_session->refs++;

    BAIL_ON_ERROR(status = lwmsg_task_new(
        session->peer->task_manager,
        session->peer->connect_tasks,
        lwmsg_peer_assoc_session_run,
        my_session,
        &my_session->event_task));
    *assoc_session = my_session;

done:

    return status;

error:

    if (my_session)
    {
        my_session->session = NULL;
        lwmsg_peer_assoc_session_delete(my_session);
    }

    goto done;
}

static
void
lwmsg_peer_assoc_endpoint_delete_self(
    PeerAssocEndpoint* endpoint
    )
{
    if (endpoint->event_task)
    {
        lwmsg_task_release(endpoint->event_task);
    }

    if (endpoint->endpoint)
    {
        unlink(endpoint->endpoint);
        free(endpoint->endpoint);
    }

    if (endpoint->fd >= 0)
    {
        close(endpoint->fd);
    }

    free(endpoint);
}

void
lwmsg_peer_assoc_endpoint_delete(
    PeerAssocEndpoint* endpoint
    )
{
    if (endpoint->event_task)
    {
        /* Just cancel the task, it will take care of
           cleaning up after itself */
        lwmsg_task_cancel(endpoint->event_task);
    }
    else
    {
        lwmsg_peer_assoc_endpoint_delete_self(endpoint);
    }
}

static
LWMsgBool
lwmsg_peer_assoc_session_subject_to_timeout(
    LWMsgPeer* peer,
    PeerAssocSession* session,
    LWMsgTaskTrigger trigger
    )
{
    size_t handle_count = 0;
    size_t num_clients = 0;

    if (session->state == PEER_TASK_DISPATCH)
    {
        handle_count = lwmsg_session_get_handle_count((LWMsgSession*) session->session);
        num_clients = lwmsg_peer_get_num_clients(peer);

        return handle_count == 0 && num_clients == peer->max_clients &&
            lwmsg_hash_get_count(&session->incoming_calls) == 0;
    }
    else
    {
        return LWMSG_TRUE;
    }

    return LWMSG_TRUE;
}

static
void
lwmsg_peer_assoc_session_set_timeout(
    LWMsgPeer* peer,
    PeerAssocSession* session,
    LWMsgTime* timeout,
    LWMsgTaskTrigger trigger,
    LWMsgTaskTrigger* next_trigger,
    LWMsgTime* next_timeout
    )
{
    *next_timeout = *timeout;

    if (lwmsg_time_is_positive(timeout) &&
        lwmsg_peer_assoc_session_subject_to_timeout(peer, session, trigger))
    {
        *next_trigger |= LWMSG_TASK_TRIGGER_TIME;
    }
}

static
LWMsgStatus
lwmsg_peer_assoc_session_handle_assoc_error(
    LWMsgPeer* peer,
    PeerAssocSession* session,
    LWMsgStatus status
    )
{
    LWMsgSessionString sessid = {0};

    lwmsg_peer_session_string_for_session((LWMsgSession*) session->session, sessid);

    if (status)
    {
        LOCK_MUTEX(&session->session->lock);
        session->status = status;
        UNLOCK_MUTEX(&session->session->lock);
    }

    switch (status)
    {
    case LWMSG_STATUS_SUCCESS:
        break;
    case LWMSG_STATUS_CONNECTION_REFUSED:
    case LWMSG_STATUS_PEER_CLOSE:
    case LWMSG_STATUS_PEER_RESET:
    case LWMSG_STATUS_PEER_ABORT:
        LWMSG_LOG_VERBOSE(peer->context, "(session:%s) Dropping: %s",
                          sessid,
                          lwmsg_status_name(status));
        session->state = PEER_TASK_DROP;
        status = LWMSG_STATUS_SUCCESS;
        break;
    case LWMSG_STATUS_TIMEOUT:
        LWMSG_LOG_VERBOSE(peer->context, "(session:%s) Resetting: %s",
                          sessid,
                          lwmsg_status_name(status));
        session->state = PEER_TASK_BEGIN_RESET;
        status = LWMSG_STATUS_SUCCESS;
    default:
        LWMSG_LOG_VERBOSE(peer->context, "(session:%s) Dropping: %s",
                          sessid,
                          lwmsg_status_name(status));
        session->state = PEER_TASK_BEGIN_CLOSE;
        status = LWMSG_STATUS_SUCCESS;
        break;
    }

    return status;
}

static
LWMsgStatus
lwmsg_peer_assoc_session_handle_call_error(
    PeerAssocSession* session,
    PeerAssocCall* call,
    LWMsgStatus status
    )
{
    call->status = status;
    call->state = PEER_CALL_DISPATCHED | PEER_CALL_COMPLETED;
    call->params.incoming.out.tag = LWMSG_TAG_INVALID;
    call->params.incoming.out.data = NULL;

    return LWMSG_STATUS_SUCCESS;
}

static
void
lwmsg_peer_assoc_endpoint_run_listen(
    LWMsgTask* _task,
    void* data,
    LWMsgTaskTrigger trigger,
    LWMsgTaskTrigger* next_trigger,
    LWMsgTaskTime* next_timeout
    )
{
    LWMsgStatus status = LWMSG_STATUS_SUCCESS;
    PeerAssocEndpoint* endpoint = data;
    int client_fd = -1;
    LWMsgBool slot = LWMSG_FALSE;
    int err = 0;

    if (trigger & LWMSG_TASK_TRIGGER_INIT)
    {
        BAIL_ON_ERROR(status = lwmsg_task_set_trigger_fd(endpoint->event_task, endpoint->fd));
        if (endpoint->endpoint)
        {
            LWMSG_LOG_VERBOSE(endpoint->peer->context, "Listening on endpoint %s", endpoint->endpoint);
        }
        else
        {
            LWMSG_LOG_VERBOSE(endpoint->peer->context, "Listening on fd %i", endpoint->fd);
        }
    }

    if (trigger & LWMSG_TASK_TRIGGER_CANCEL)
    {
        if (endpoint->endpoint)
        {
            LWMSG_LOG_VERBOSE(endpoint->peer->context, "Shutting down listen endpoint %s", endpoint->endpoint);
        }
        else
        {
            LWMSG_LOG_VERBOSE(endpoint->peer->context, "Shutting down listen fd %i", endpoint->fd);
        }
        lwmsg_task_unset_trigger_fd(endpoint->event_task, endpoint->fd);
        lwmsg_peer_assoc_endpoint_delete_self(endpoint);
        *next_trigger = 0;
        goto done;
    }

    *next_trigger = LWMSG_TASK_TRIGGER_YIELD;

    if ((slot = lwmsg_peer_acquire_client_slot(endpoint->peer)))
    {
        do
        {
            client_fd = accept(endpoint->fd, NULL, NULL);

            if (client_fd < 0)
            {
                switch (errno)
                {
                case EAGAIN:
#if defined(EWOULDBLOCK) && (EWOULDBLOCK != EAGAIN)
                case EWOULDBLOCK:
#endif
                    /* We are blocked, wake up when fd is readable */
                    *next_trigger = LWMSG_TASK_TRIGGER_FD_READABLE;
                    goto done;
                case EINTR:
                case ECONNABORTED:
                    /* Trivially retriable errors.  Ignore them and loop again */
                    continue;
                default:
                    err = errno;
                    LWMSG_LOG_ERROR(endpoint->peer->context, "System error on accept(): %i", err);
                    BAIL_ON_ERROR(status = lwmsg_status_map_errno(err));
                }
            }
        } while (client_fd < 0);

        BAIL_ON_ERROR(status = lwmsg_peer_accept_fd_internal(
                          endpoint->peer,
                          LWMSG_ENDPOINT_LOCAL,
                          client_fd,
                          endpoint->endpoint));
        slot = LWMSG_FALSE;
    }
    else
    {
        /* We've run out of client slots, so stop accepting clients
           until we are explicitly woken up */
        *next_trigger = LWMSG_TASK_TRIGGER_EXPLICIT;
        goto done;
    }

done:

    if (slot)
    {
        lwmsg_peer_release_client_slot(endpoint->peer);
    }

    return;

error:

    if (client_fd >= 0)
    {
        close(client_fd);
    }

    /* If the listen session aborts, the server will be left in a state
       where it is running but cannot be contacted.  Invoke the
       exception function set on the server to give the application
       a chance to bail out if it wishes.  Also, attempt to keep running
       if the error appears to be recoverable */
    if (endpoint->peer->except)
    {
        endpoint->peer->except(endpoint->peer, status, endpoint->peer->except_data);
    }

    switch (status)
    {
    case LWMSG_STATUS_BUSY:
    case LWMSG_STATUS_MEMORY:
    case LWMSG_STATUS_RESOURCE_LIMIT:
        *next_trigger = LWMSG_TASK_TRIGGER_YIELD;
        break;
    default:
        *next_trigger = 0;
        break;
    }

    goto done;
}

static
LWMsgStatus
lwmsg_peer_assoc_session_run_accept(
    LWMsgPeer* peer,
    PeerAssocSession* session,
    LWMsgTaskTrigger trigger,
    LWMsgTaskTrigger* next_trigger,
    LWMsgTime* next_timeout
    )
{
    LWMsgStatus status = LWMSG_STATUS_SUCCESS;

    /* We already have the fd when accepting a connection, so set it up for events now */
    BAIL_ON_ERROR(status = lwmsg_task_set_trigger_fd(session->event_task, CONNECTION_PRIVATE(session->assoc)->fd));

    status = lwmsg_assoc_accept(session->assoc, (LWMsgSession*) session->session);

    switch (status)
    {
    case LWMSG_STATUS_SUCCESS:
        session->state = PEER_TASK_DISPATCH;
        BAIL_ON_ERROR(status = lwmsg_peer_assoc_session_log_accept(session));
        break;
    case LWMSG_STATUS_PENDING:
        session->state = PEER_TASK_FINISH_ACCEPT;
        lwmsg_peer_assoc_session_set_timeout(peer, session, &peer->timeout.establish, trigger, next_trigger, next_timeout);
        break;
    default:
        BAIL_ON_ERROR(status = lwmsg_peer_assoc_session_handle_assoc_error(
                          peer,
                          session,
                          status));
        break;
    }

done:

    return status;

error:

    goto done;
}

static
LWMsgStatus
lwmsg_peer_assoc_session_run_connect(
    LWMsgPeer* peer,
    PeerAssocSession* session,
    LWMsgTaskTrigger trigger,
    LWMsgTaskTrigger* next_trigger,
    LWMsgTime* next_timeout
    )
{
    LWMsgStatus status = LWMSG_STATUS_SUCCESS;

    status = lwmsg_assoc_connect(session->assoc, (LWMsgSession*) session->session);

    switch (status)
    {
    case LWMSG_STATUS_SUCCESS:
        session->state = PEER_TASK_DISPATCH;
        BAIL_ON_ERROR(status = lwmsg_peer_assoc_session_log_connect(session));
        /* Set up the fd the assoc just created for events */
        BAIL_ON_ERROR(status = lwmsg_task_set_trigger_fd(session->event_task, CONNECTION_PRIVATE(session->assoc)->fd));
        break;
    case LWMSG_STATUS_PENDING:
        session->state = PEER_TASK_FINISH_CONNECT;
        /* Even if the connect pended, the fd is now available.  Set it up for events */
        BAIL_ON_ERROR(status = lwmsg_task_set_trigger_fd(session->event_task, CONNECTION_PRIVATE(session->assoc)->fd));
        /* Restore status code */
        status = LWMSG_STATUS_PENDING;
        lwmsg_peer_assoc_session_set_timeout(peer, session, &peer->timeout.establish, trigger, next_trigger, next_timeout);
        break;
    default:
        BAIL_ON_ERROR(status = lwmsg_peer_assoc_session_handle_assoc_error(
                          peer,
                          session,
                          status));
        break;
    }

done:

    return status;

error:

    goto done;
}

static
LWMsgStatus
lwmsg_peer_assoc_session_rundown(
    LWMsgPeer* peer,
    PeerAssocSession* session,
    LWMsgTaskTrigger trigger,
    LWMsgTaskTrigger* next_trigger,
    LWMsgTime* next_timeout
    )
{
    LWMsgStatus status = LWMSG_STATUS_SUCCESS;
    LWMsgHashIter iter = {0};
    PeerAssocCall* call = NULL;
    LWMsgMessage cancel = LWMSG_MESSAGE_INITIALIZER;
    LWMsgMessage message = LWMSG_MESSAGE_INITIALIZER;

    LOCK_MUTEX(&session->session->lock);

    // Make sure an error is set on the session to inhibit further calls
    // in lwmsg_peer_assoc_call_dispatch_outgoing and prevent concurrent
    // modification to session->outgoing_calls while we iterate it.
    if (session->status == LWMSG_STATUS_SUCCESS)
    {
        session->status = LWMSG_STATUS_CANCELLED;
    }

    lwmsg_hash_iter_begin(&session->incoming_calls, &iter);
    while ((call = lwmsg_hash_iter_next(&session->incoming_calls, &iter)))
    {
        if (call->state & PEER_CALL_COMPLETED)
        {
            /* Destroy in call parameters */
            message.tag = call->params.incoming.in.tag;
            message.data = call->params.incoming.in.data;
            lwmsg_assoc_destroy_message(session->assoc, &message);

            /* Destroy out call parameters */
            message.tag = call->params.incoming.out.tag;
            message.data = call->params.incoming.out.data;
            lwmsg_assoc_destroy_message(session->assoc, &message);

            lwmsg_hash_remove_entry(&session->incoming_calls, call);
            lwmsg_peer_assoc_call_release(call);
        }
        else if (!(call->state & PEER_CALL_CANCEL_START))
        {
            lwmsg_peer_assoc_call_cancel_incoming(call);
        }
    }
    lwmsg_hash_iter_end(&session->incoming_calls, &iter);

    lwmsg_hash_iter_begin(&session->outgoing_calls, &iter);
    while ((call = lwmsg_hash_iter_next(&session->outgoing_calls, &iter)))
    {
        cancel.status = session->status;
        lwmsg_peer_assoc_call_complete_outgoing(call, &cancel);
    }
    lwmsg_hash_iter_end(&session->outgoing_calls, &iter);

    if (lwmsg_hash_get_count(&session->incoming_calls) != 0 ||
        lwmsg_hash_get_count(&session->outgoing_calls) != 0)
    {
        *next_trigger = LWMSG_TASK_TRIGGER_EXPLICIT;
        BAIL_ON_ERROR(status = LWMSG_STATUS_PENDING);
    }

error:

    UNLOCK_MUTEX(&session->session->lock);

    return status;
}

static
LWMsgStatus
lwmsg_peer_assoc_session_run_shutdown(
    LWMsgPeer* peer,
    PeerAssocSession* session,
    LWMsgTaskTrigger trigger,
    LWMsgTaskTrigger* next_trigger,
    LWMsgTime* next_timeout
    )
{
    LWMsgStatus status = LWMSG_STATUS_SUCCESS;
    int fd = CONNECTION_PRIVATE(session->assoc)->fd;

    /* Before we close the assoc, we need to free any data it allocated */

    /* First, make sure any calls are canceled, completed, and freed */
    BAIL_ON_ERROR(status = lwmsg_peer_assoc_session_rundown(peer, session, trigger, next_trigger, next_timeout));

    /* Destroy any incoming message we never got around to dispatching */
    lwmsg_assoc_destroy_message(session->assoc, &session->incoming_message);

    /* Destroy any outgoing message we never got around to sending */
    if (session->destroy_outgoing)
    {
        lwmsg_assoc_destroy_message(session->assoc, &session->outgoing_message);
    }

    /* We are going to close the fd, so unset it now */
    if (fd >= 0)
    {
        lwmsg_task_unset_trigger_fd(session->event_task, fd);
    }

    switch (session->state)
    {
    case PEER_TASK_BEGIN_CLOSE:
        status = lwmsg_assoc_close(session->assoc);
        break;
    case PEER_TASK_BEGIN_RESET:
        status = lwmsg_assoc_reset(session->assoc);
        break;
    default:
        BAIL_ON_ERROR(status = LWMSG_STATUS_INTERNAL);
    }

    switch (status)
    {
    case LWMSG_STATUS_SUCCESS:
        session->state = PEER_TASK_DROP;
        break;
    case LWMSG_STATUS_PENDING:
        /* Oops, we still need to wait for fd events, so set it on the task again */
        if (fd >= 0)
        {
            BAIL_ON_ERROR(status = lwmsg_task_set_trigger_fd(session->event_task, fd));
        }
        session->state = PEER_TASK_FINISH_CLOSE;
        lwmsg_peer_assoc_session_set_timeout(peer, session, &peer->timeout.establish, trigger, next_trigger, next_timeout);
        break;
    default:
        BAIL_ON_ERROR(status);
        break;
    }

done:

    return status;

error:

    goto done;
}

static
void
lwmsg_peer_assoc_session_cancel_call(
    PeerAssocSession* session,
    LWMsgCookie cookie
    )
{
    PeerAssocCall* call = NULL;

    call = lwmsg_hash_find_key(&session->incoming_calls, &cookie);

    if (call)
    {
        lwmsg_peer_assoc_session_log_message(session, &session->incoming_message, LWMSG_FALSE);
        lwmsg_peer_assoc_call_cancel_incoming(call);
    }
}

static
void
lwmsg_peer_assoc_session_complete_call(
    PeerAssocSession* session,
    LWMsgCookie cookie
    )
{
    PeerAssocCall* call = NULL;

    call = lwmsg_hash_find_key(&session->outgoing_calls, &cookie);

    if (call)
    {
        lwmsg_peer_assoc_session_log_message(session, &session->incoming_message, LWMSG_TRUE);
        lwmsg_peer_assoc_call_complete_outgoing(call, &session->incoming_message);
    }
}

static
LWMsgStatus
lwmsg_peer_assoc_session_dispatch_incoming_message(
    PeerAssocSession* session
    )
{
    LWMsgStatus status = LWMSG_STATUS_SUCCESS;
    LWMsgDispatchSpec* spec = NULL;
    LWMsgTag tag = session->incoming_message.tag;
    LWMsgPeer* peer = session->session->peer;
    PeerAssocCall* call = NULL;

    /* If the incoming message is a reply to one of our calls,
       find it and complete it */
    if (session->incoming_message.flags & LWMSG_MESSAGE_FLAG_REPLY)
    {
        lwmsg_peer_assoc_session_complete_call(session, session->incoming_message.cookie);
        goto error;
    }
    /* If the incoming message is a control message... */
    else if (session->incoming_message.flags & LWMSG_MESSAGE_FLAG_CONTROL)
    {
        switch (session->incoming_message.status)
        {
        case LWMSG_STATUS_CANCELLED:
            /* The caller cancelled a previous call, so find it and cancel it here as well */
            lwmsg_peer_assoc_session_cancel_call(session, session->incoming_message.cookie);
            lwmsg_assoc_destroy_message(session->assoc, &session->incoming_message);
            goto error;
        default:
            /* Silently drop unknown control messages */
            lwmsg_assoc_destroy_message(session->assoc, &session->incoming_message);
            goto error;
        }
    }

    lwmsg_peer_assoc_session_log_message(session, &session->incoming_message, LWMSG_FALSE);

    /* Check if this cookie is already in use */
    if (lwmsg_hash_find_key(&session->incoming_calls, &session->incoming_message.cookie))
    {
        /* This association is toast */
        status = lwmsg_peer_assoc_session_handle_assoc_error(
            peer,
            session,
            LWMSG_STATUS_MALFORMED);
        lwmsg_assoc_destroy_message(session->assoc, &session->incoming_message);
        goto error;
    }

    /* Create a call structure to track call */
    BAIL_ON_ERROR(status = lwmsg_peer_assoc_call_new(session, &call));
    call->base.is_outgoing = LWMSG_FALSE;
    call->params.incoming.in.tag = LWMSG_TAG_INVALID;
    call->params.incoming.out.tag = LWMSG_TAG_INVALID;

    /* The call structure holds a reference to the session */
    session->refs++;

    /* Set cookie and insert into incoming call table */
    call->cookie = session->incoming_message.cookie;
    lwmsg_hash_insert_entry(&session->incoming_calls, call);

    /* If the incoming message is synthetic for some bizarre reason,
       just echo back the status code to the caller */
    if (session->incoming_message.flags & LWMSG_MESSAGE_FLAG_SYNTHETIC)
    {
        status = lwmsg_peer_assoc_session_handle_call_error(
            session,
            call,
            session->incoming_message.status);
        lwmsg_assoc_destroy_message(session->assoc, &session->incoming_message);
        goto error;
    }

    /* Make sure the tag is within the bounds of the dispatch vector */
    if (tag < 0 || tag >= peer->dispatch.vector_length)
    {
        status = lwmsg_peer_assoc_session_handle_call_error(
            session,
            call,
            LWMSG_STATUS_MALFORMED);
        lwmsg_assoc_destroy_message(session->assoc, &session->incoming_message);
        goto error;
    }

    /* Make sure the call is supported */
    spec = peer->dispatch.vector[tag];
    if (spec == NULL)
    {
        status = lwmsg_peer_assoc_session_handle_call_error(
            session,
            call,
            LWMSG_STATUS_UNSUPPORTED);
        lwmsg_assoc_destroy_message(session->assoc, &session->incoming_message);
        goto error;
    }

    /* Dispatch the call */
    status = lwmsg_peer_assoc_call_dispatch_incoming(
        call,
        spec,
        peer->dispatch_data,
        &session->incoming_message);
    switch (status)
    {
    case LWMSG_STATUS_SUCCESS:
        break;
    case LWMSG_STATUS_PENDING:
        status = LWMSG_STATUS_SUCCESS;
        break;
    default:
        status = lwmsg_peer_assoc_session_handle_call_error(session, call, status);
        BAIL_ON_ERROR(status);
        break;
    }

error:

    return status;
}

static
LWMsgStatus
lwmsg_peer_assoc_session_run_finish(
    LWMsgPeer* peer,
    PeerAssocSession* session,
    LWMsgTaskTrigger trigger,
    LWMsgTaskTrigger* next_trigger,
    LWMsgTime* next_timeout
    )
{
    LWMsgStatus status = LWMSG_STATUS_SUCCESS;
    int fd = CONNECTION_PRIVATE(session->assoc)->fd;

#if 0
    /* When the peer runs out of available client slots, it will
       wake all of its tasks so that it can begin enforcing timeouts
       where it previously did not bother.  Tell the task manager to
       begin waking us up on timeout events if this is the case */
    if ((trigger & LWMSG_TASK_TRIGGER_EXPLICIT) &&
        lwmsg_peer_assoc_session_subject_to_timeout(peer, session, trigger))
    {
        *next_trigger |= LWMSG_TASK_TRIGGER_TIME;
    }
#endif

    /* Did we hit a timeout? */
    if (trigger & LWMSG_TASK_TRIGGER_TIME)
    {
        BAIL_ON_ERROR(status = lwmsg_peer_assoc_session_handle_assoc_error(
                          peer,
                          session,
                          LWMSG_STATUS_TIMEOUT));
    }
    /* Are we shutting down? */
    else if (trigger & LWMSG_TASK_TRIGGER_CANCEL)
    {
        /* Drop the session unceremoniously in the interest
           of shutting down quickly */
        session->state = PEER_TASK_DROP;
    }
    /* Is the task unblocked? */
    else if (!session->recv_blocked || !session->send_blocked)
    {
        switch (session->state)
        {
        case PEER_TASK_FINISH_CLOSE:
        case PEER_TASK_FINISH_RESET:
            /* We are going to close the fd, so unset it on session */
            if (fd >= 0)
            {
                lwmsg_task_unset_trigger_fd(session->event_task, fd);
            }
            break;
        default:
            break;
        }

        status = lwmsg_assoc_finish(session->assoc, NULL);

        switch (status)
        {
        case LWMSG_STATUS_SUCCESS:
            switch (session->state)
            {
            case PEER_TASK_FINISH_ACCEPT:
                BAIL_ON_ERROR(status = lwmsg_peer_assoc_session_log_accept(session));
                session->state = PEER_TASK_DISPATCH;
                break;
            case PEER_TASK_FINISH_CONNECT:
                BAIL_ON_ERROR(status = lwmsg_peer_assoc_session_log_connect(session));
                session->state = PEER_TASK_DISPATCH;
                break;
            case PEER_TASK_FINISH_CLOSE:
            case PEER_TASK_FINISH_RESET:
                session->state = PEER_TASK_DROP;
                break;
            default:
                break;
            }
            break;
        case LWMSG_STATUS_PENDING:
            switch (session->state)
            {
            case PEER_TASK_FINISH_CLOSE:
            case PEER_TASK_FINISH_RESET:
                /* Nevermind, set the fd on the task again */
                if (fd >= 0)
                {
                    BAIL_ON_ERROR(status = lwmsg_task_set_trigger_fd(session->event_task, fd));
                }
                break;
            default:
                break;
            }
            break;
        default:
            BAIL_ON_ERROR(status = lwmsg_peer_assoc_session_handle_assoc_error(
                              peer,
                              session,
                              status));
            break;
        }
    }
    /* Nothing to do */
    else
    {
        BAIL_ON_ERROR(status = LWMSG_STATUS_PENDING);
    }

error:

    return status;
}

static
LWMsgStatus
lwmsg_peer_assoc_session_finish_transceive(
    LWMsgPeer* peer,
    PeerAssocSession* session
    )
{
    LWMsgStatus status = LWMSG_STATUS_PENDING;
    LWMsgMessage* message = NULL;

    while ((!session->recv_blocked && session->incoming) ||
           (!session->send_blocked && session->outgoing))
    {
        status = lwmsg_assoc_finish(session->assoc, &message);
        switch (status)
        {
        case LWMSG_STATUS_SUCCESS:
            if (message == &session->incoming_message)
            {
                session->incoming = LWMSG_FALSE;
                session->recv_partial = LWMSG_FALSE;
                BAIL_ON_ERROR(status = lwmsg_peer_assoc_session_dispatch_incoming_message(session));
            }
            else if (message == &session->outgoing_message)
            {
                lwmsg_peer_assoc_session_log_message(session, &session->outgoing_message, LWMSG_FALSE);
                session->outgoing = LWMSG_FALSE;
                if (session->destroy_outgoing)
                {
                    lwmsg_assoc_destroy_message(session->assoc, &session->outgoing_message);
                    session->destroy_outgoing = LWMSG_FALSE;
                }
            }
            break;
        case LWMSG_STATUS_PENDING:
            lwmsg_peer_assoc_session_update_blocked(session);
            break;
        default:
            BAIL_ON_ERROR(status);
        }
    }

error:

    return status;
}

static
LWMsgStatus
lwmsg_peer_assoc_session_dispatch_calls(
    PeerAssocSession* session
    )
{
    LWMsgStatus status = LWMSG_STATUS_PENDING;
    PeerAssocCall* call = NULL;
    LWMsgCookie cookie = 0;
    LWMsgMessage message = LWMSG_MESSAGE_INITIALIZER;
    LWMsgRing* ring = NULL;
    LWMsgSessionString sessid = {0};

    while (!lwmsg_ring_is_empty(&session->active_outgoing_calls))
    {
        lwmsg_ring_dequeue(&session->active_outgoing_calls, &ring);

        call = LWMSG_OBJECT_FROM_MEMBER(ring, PeerAssocCall, queue_ring);

        /* Undispatched but already canceled call -- complete as canceled */
        if (!(call->state & PEER_CALL_DISPATCHED) &&
            (call->state & PEER_CALL_CANCEL_START))
        {
            lwmsg_message_init(&message);
            message.status = LWMSG_STATUS_CANCELLED;

            lwmsg_peer_assoc_call_complete_outgoing(call, &message);
        }
        /* Undispatched outgoing call -- send request */
        else if (!(call->state & PEER_CALL_DISPATCHED))
        {
            call->state |= PEER_CALL_DISPATCHED;

            lwmsg_message_init(&session->outgoing_message);
            session->outgoing_message.tag = call->params.outgoing.in->tag;
            session->outgoing_message.data = call->params.outgoing.in->data;
            session->outgoing_message.cookie = call->cookie;
            session->outgoing_message.status = call->status;

            lwmsg_peer_assoc_session_log_message(session, &session->outgoing_message, LWMSG_TRUE);
            status = lwmsg_assoc_send_message(session->assoc, &session->outgoing_message);
            switch (status)
            {
            case LWMSG_STATUS_SUCCESS:
                break;
            case LWMSG_STATUS_PENDING:
                session->outgoing = LWMSG_TRUE;
                BAIL_ON_ERROR(status);
                break;
            case LWMSG_STATUS_MALFORMED:
            case LWMSG_STATUS_INVALID_HANDLE:
                /* The association should still be usable, so just fail the call */
                lwmsg_message_init(&message);
                message.status = status;
                message.flags = LWMSG_MESSAGE_FLAG_REPLY | LWMSG_MESSAGE_FLAG_SYNTHETIC;
                message.cookie = call->cookie;
                lwmsg_peer_assoc_call_complete_outgoing(call, &message);
                status = LWMSG_STATUS_SUCCESS;
                break;
            default:
                BAIL_ON_ERROR(status);
            }
        }
        else if ((call->state & PEER_CALL_DISPATCHED) &&
                 (call->state & PEER_CALL_CANCEL_DONE) &&
                 call->status != LWMSG_STATUS_CANCELLED)
        {
            /* Cancelled outgoing call -- send cancel request */
            call->status = LWMSG_STATUS_CANCELLED;

            lwmsg_message_init(&session->outgoing_message);
            session->outgoing_message.flags = LWMSG_MESSAGE_FLAG_CONTROL;
            session->outgoing_message.status = LWMSG_STATUS_CANCELLED;
            session->outgoing_message.cookie = call->cookie;

            lwmsg_peer_assoc_session_log_message(session, &session->outgoing_message, LWMSG_TRUE);
            status = lwmsg_assoc_send_message(session->assoc, &session->outgoing_message);
            switch (status)
            {
            case LWMSG_STATUS_SUCCESS:
                break;
            case LWMSG_STATUS_PENDING:
                session->outgoing = LWMSG_TRUE;
                BAIL_ON_ERROR(status);
                break;
            default:
                BAIL_ON_ERROR(status);
            }
        }
    }

    while (!lwmsg_ring_is_empty(&session->active_incoming_calls))
    {
        lwmsg_ring_dequeue(&session->active_incoming_calls, &ring);

        call = LWMSG_OBJECT_FROM_MEMBER(ring, PeerAssocCall, queue_ring);

        /* Completed incoming call -- send reply */
        if ((call->state & PEER_CALL_COMPLETED) &&
            (call->state & PEER_CALL_DISPATCHED))
        {
            /* Trace call completion */
            if (call->session->session->peer->trace_end)
            {
                call->session->session->peer->trace_end(
                    LWMSG_CALL(call),
                    &call->params.incoming.out,
                    call->status,
                    call->session->session->peer->trace_data);
            }

            lwmsg_message_init(&session->outgoing_message);
            session->outgoing_message.flags = LWMSG_MESSAGE_FLAG_REPLY;
            session->outgoing_message.tag = call->params.incoming.out.tag;
            session->outgoing_message.data = call->params.incoming.out.data;
            session->outgoing_message.cookie = call->cookie;
            session->outgoing_message.status = call->status;

            cookie = call->cookie;

            /* It's now safe to destroy the incoming call parameters */
            message.tag = call->params.incoming.in.tag;
            message.data = call->params.incoming.in.data;
            lwmsg_assoc_destroy_message(session->assoc, &message);

            /* Delete the call structure */
            lwmsg_hash_remove_entry(&session->incoming_calls, call);
            lwmsg_peer_assoc_call_release(call);

            do
            {
                status = lwmsg_assoc_send_message(session->assoc, &session->outgoing_message);
                switch (status)
                {
                case LWMSG_STATUS_SUCCESS:
                    lwmsg_peer_assoc_session_log_message(session, &session->outgoing_message, LWMSG_FALSE);
                    lwmsg_assoc_destroy_message(session->assoc, &session->outgoing_message);
                    break;
                case LWMSG_STATUS_PENDING:
                    session->outgoing = LWMSG_TRUE;
                    session->destroy_outgoing = LWMSG_TRUE;
                    BAIL_ON_ERROR(status);
                    break;
                case LWMSG_STATUS_MALFORMED:
                case LWMSG_STATUS_INVALID_HANDLE:
                case LWMSG_STATUS_OVERFLOW:
                case LWMSG_STATUS_UNDERFLOW:
                    /* Our reply could not be sent because the dispatch function gave us
                       bogus response parameters.  Send a synthetic reply instead so the
                       caller at least knows the call is complete */
                    lwmsg_peer_session_string_for_session((LWMsgSession*) session->session, sessid);
                    LWMSG_LOG_WARNING(session->session->peer->context,
                                      "(session:%s >> %u) Response payload could not be sent: %s",
                                      sessid,
                                      cookie,
                                      lwmsg_status_name(status));
                    lwmsg_assoc_destroy_message(session->assoc, &session->outgoing_message);
                    lwmsg_message_init(&session->outgoing_message);
                    session->outgoing_message.flags = LWMSG_MESSAGE_FLAG_REPLY | LWMSG_MESSAGE_FLAG_SYNTHETIC;
                    session->outgoing_message.status = status;
                    session->outgoing_message.cookie = cookie;
                    status = LWMSG_STATUS_AGAIN;
                    break;
                default:
                    BAIL_ON_ERROR(status);
                }
            } while (status == LWMSG_STATUS_AGAIN);
        }
    }

error:

    return status;
}

static
LWMsgStatus
lwmsg_peer_assoc_session_run_dispatch(
    LWMsgPeer* peer,
    PeerAssocSession* session,
    LWMsgTaskTrigger* trigger,
    LWMsgTaskTrigger* next_trigger,
    LWMsgTime* next_timeout
    )
{
    LWMsgStatus status = LWMSG_STATUS_SUCCESS;
    LWMsgStatus finish_status = LWMSG_STATUS_PENDING;
    LWMsgStatus recv_status = LWMSG_STATUS_PENDING;
    LWMsgStatus send_status = LWMSG_STATUS_PENDING;
    LWMsgTime* timeout = NULL;

    LOCK_MUTEX(&session->session->lock);

    /* Did we hit a timeout? */
    if (*trigger & LWMSG_TASK_TRIGGER_TIME)
    {
        BAIL_ON_ERROR(status = lwmsg_peer_assoc_session_handle_assoc_error(
                          peer,
                          session,
                          LWMSG_STATUS_TIMEOUT));
    }
    /* Are we shutting down? */
    else if (*trigger & LWMSG_TASK_TRIGGER_CANCEL)
    {
        /* Drop the session unceremoniously in the interest
           of shutting down quickly */
        session->state = PEER_TASK_DROP;
    }
    else
    {
        /* Try to finish any outstanding sends/receives */
        finish_status = lwmsg_peer_assoc_session_finish_transceive(peer, session);
        switch (finish_status)
        {
        case LWMSG_STATUS_SUCCESS:
        case LWMSG_STATUS_PENDING:
            break;
        default:
            BAIL_ON_ERROR(status = finish_status);
        }

        /* Try to receive and dispatch the next message */
        if (!session->incoming && !session->recv_blocked)
        {
            recv_status = lwmsg_assoc_recv_message(session->assoc, &session->incoming_message);

            switch (recv_status)
            {
            case LWMSG_STATUS_SUCCESS:
                session->recv_partial = LWMSG_FALSE;
                BAIL_ON_ERROR(status = lwmsg_peer_assoc_session_dispatch_incoming_message(session));
                break;
            case LWMSG_STATUS_PENDING:
                session->incoming = LWMSG_TRUE;
                break;
            default:
                BAIL_ON_ERROR(status = recv_status);
            }
        }

        /* Try to send any outgoing call requests or incoming call replies */
        if (!session->outgoing && !session->send_blocked)
        {
            send_status = lwmsg_peer_assoc_session_dispatch_calls(session);
            switch (send_status)
            {
            case LWMSG_STATUS_SUCCESS:
            case LWMSG_STATUS_PENDING:
                break;
            default:
                BAIL_ON_ERROR(status = send_status);
            }
        }

        /* If we could not make any progress finishing pended I/O,
           dispatching outgoing calls or incoming call replies,
           or receiving new calls, then we should go to sleep */
        if (finish_status == LWMSG_STATUS_PENDING &&
            send_status == LWMSG_STATUS_PENDING &&
            recv_status == LWMSG_STATUS_PENDING)
        {
            if ((!session->incoming || !session->recv_partial) && !session->outgoing)
            {
                timeout = &peer->timeout.idle;
            }
            else
            {
                timeout = &peer->timeout.message;
            }
            lwmsg_peer_assoc_session_set_timeout(peer, session, timeout, *trigger, next_trigger, next_timeout);
            BAIL_ON_ERROR(status = LWMSG_STATUS_PENDING);
        }
    }

error:

    UNLOCK_MUTEX(&session->session->lock);

    switch (status)
    {
    case LWMSG_STATUS_SUCCESS:
    case LWMSG_STATUS_PENDING:
        break;
    default:
        status = lwmsg_peer_assoc_session_handle_assoc_error(
            peer,
            session,
            status);
        break;
    }

    return status;
}

static
LWMsgStatus
lwmsg_peer_assoc_session_run_drop(
    LWMsgPeer* peer,
    PeerAssocSession* session,
    LWMsgTaskTrigger trigger,
    LWMsgTaskTrigger* next_trigger,
    LWMsgTime* next_timeout
    )
{
    LWMsgStatus status = LWMSG_STATUS_SUCCESS;
    int fd = CONNECTION_PRIVATE(session->assoc)->fd;

    BAIL_ON_ERROR(status = lwmsg_peer_assoc_session_rundown(peer, session, trigger, next_trigger, next_timeout));

    if (fd >= 0)
    {
        lwmsg_task_unset_trigger_fd(session->event_task, fd);
    }

    session->state = PEER_TASK_DONE;

error:

    return status;
}

static
void
lwmsg_peer_assoc_session_run(
    LWMsgTask* _task,
    void* data,
    LWMsgTaskTrigger trigger,
    LWMsgTaskTrigger* next_trigger,
    LWMsgTaskTime* next_timeout
    )
{
    LWMsgStatus status = LWMSG_STATUS_SUCCESS;
    PeerAssocSession* session = (PeerAssocSession*) data;
    LWMsgPeer* peer = session->session->peer;
    LWMsgTime my_timeout = {-1, -1};
    siginfo_t siginfo = {0};

    if (*next_timeout >= 0)
    {
        my_timeout.seconds = *next_timeout / 1000000000ll;
        my_timeout.microseconds = (*next_timeout % 1000000000ll) / 1000;
    }

    if (trigger & LWMSG_TASK_TRIGGER_INIT)
    {
        BAIL_ON_ERROR(status = lwmsg_task_set_unix_signal(
            _task,
            SIGUSR1,
            LWMSG_TRUE));
    }

    if (trigger & LWMSG_TASK_TRIGGER_UNIX_SIGNAL)
    {
        while (lwmsg_task_next_unix_signal(_task, &siginfo))
        {
            if (siginfo.si_signo == SIGUSR1 && session->assoc)
            {
                LOCK_MUTEX(&session->session->lock);
                BAIL_ON_ERROR(status = lwmsg_peer_assoc_session_log_state(session));
                UNLOCK_MUTEX(&session->session->lock);
            }
        }
    }

    if (trigger & LWMSG_TASK_TRIGGER_FD_READABLE)
    {
        session->recv_blocked = LWMSG_FALSE;
        session->recv_partial = LWMSG_TRUE;
    }

    if (trigger & LWMSG_TASK_TRIGGER_FD_WRITABLE)
    {
        session->send_blocked = LWMSG_FALSE;
    }

    *next_trigger = LWMSG_TASK_TRIGGER_EXPLICIT;

    switch (session->state)
    {
    case PEER_TASK_BEGIN_ACCEPT:
        BAIL_ON_ERROR(status = lwmsg_peer_assoc_session_run_accept(
            peer,
            session,
            trigger,
            next_trigger,
            &my_timeout));
        break;
    case PEER_TASK_BEGIN_CONNECT:
        BAIL_ON_ERROR(status = lwmsg_peer_assoc_session_run_connect(
            peer,
            session,
            trigger,
            next_trigger,
            &my_timeout));
        break;
    case PEER_TASK_DISPATCH:
        BAIL_ON_ERROR(status = lwmsg_peer_assoc_session_run_dispatch(
            peer,
            session,
            &trigger,
            next_trigger,
            &my_timeout));
        break;
    case PEER_TASK_BEGIN_CLOSE:
    case PEER_TASK_BEGIN_RESET:
        BAIL_ON_ERROR(status = lwmsg_peer_assoc_session_run_shutdown(
            peer,
            session,
            trigger,
            next_trigger,
            &my_timeout));
        break;
    case PEER_TASK_FINISH_ACCEPT:
    case PEER_TASK_FINISH_CONNECT:
    case PEER_TASK_FINISH_CLOSE:
    case PEER_TASK_FINISH_RESET:
        BAIL_ON_ERROR(status = lwmsg_peer_assoc_session_run_finish(
            peer,
            session,
            trigger,
            next_trigger,
            &my_timeout));
        break;
    case PEER_TASK_DROP:
        BAIL_ON_ERROR(status = lwmsg_peer_assoc_session_run_drop(
            peer,
            session,
            trigger,
            next_trigger,
            &my_timeout));
        break;
    case PEER_TASK_DONE:
        lwmsg_peer_assoc_session_release(session);
        *next_trigger = 0;
        return;
    default:
        break;
    }

error:

    switch (status)
    {
    case LWMSG_STATUS_SUCCESS:
    case LWMSG_STATUS_PENDING:
        lwmsg_peer_assoc_session_update_blocked(session);

        if (session->send_blocked)
        {
            *next_trigger |= LWMSG_TASK_TRIGGER_FD_WRITABLE;
        }

        if (session->recv_blocked)
        {
            *next_trigger |= LWMSG_TASK_TRIGGER_FD_READABLE;
        }

        if (status == LWMSG_STATUS_SUCCESS)
        {
            *next_trigger |= LWMSG_TASK_TRIGGER_YIELD;
        }

        *next_trigger |= LWMSG_TASK_TRIGGER_UNIX_SIGNAL;
        break;
    case LWMSG_STATUS_CANCELLED:
        session->state = PEER_TASK_DONE;
        *next_trigger = LWMSG_TASK_TRIGGER_YIELD;
        break;
    default:
        LWMSG_LOG_ERROR(peer->context, "Caught error: %s (%i)", lwmsg_status_name(status), status);

        session->state = PEER_TASK_DONE;
        *next_trigger = LWMSG_TASK_TRIGGER_YIELD;
        break;
    }

    *next_timeout = lwmsg_time_is_positive(&my_timeout) ?
        my_timeout.seconds * 1000000000ll + my_timeout.microseconds * 1000
        : 0;
}
