/* server.c: AFS server record management
 *
 * Copyright (C) 2002 Red Hat, Inc. All Rights Reserved.
 * Written by David Howells (dhowells@redhat.com)
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version
 * 2 of the License, or (at your option) any later version.
 */

#include <linux/sched.h>
#include <linux/slab.h>
#include <rxrpc/peer.h>
#include <rxrpc/connection.h>
#include "volume.h"
#include "cell.h"
#include "server.h"
#include "transport.h"
#include "vlclient.h"
#include "kafstimod.h"
#include "internal.h"

spinlock_t afs_server_peer_lock = SPIN_LOCK_UNLOCKED;

#define FS_SERVICE_ID		1	/* AFS Volume Location Service ID */
#define VL_SERVICE_ID		52	/* AFS Volume Location Service ID */

static void __afs_server_timeout(afs_timer_t *timer)
{
	afs_server_t *server = list_entry(timer,afs_server_t,timeout);

	_debug("SERVER TIMEOUT [%p{u=%d}]",server,atomic_read(&server->usage));

	afs_server_do_timeout(server);
}

static const struct afs_timer_ops afs_server_timer_ops = {
	.timed_out	= __afs_server_timeout,
};

/*****************************************************************************/
/*
 * lookup a server record in a cell
 * - TODO: search the cell's server list
 */
int afs_server_lookup(afs_cell_t *cell, const struct in_addr *addr, afs_server_t **_server)
{
	struct list_head *_p;
	afs_server_t *server, *active, *zombie;
	int loop;

	_enter("%p,%08x,",cell,ntohl(addr->s_addr));

	/* allocate and initialise a server record */
	server = kmalloc(sizeof(afs_server_t),GFP_KERNEL);
	if (!server) {
		_leave(" = -ENOMEM");
		return -ENOMEM;
	}

	memset(server,0,sizeof(afs_server_t));
	atomic_set(&server->usage,1);

	INIT_LIST_HEAD(&server->link);
	init_rwsem(&server->sem);
	INIT_LIST_HEAD(&server->fs_callq);
	spin_lock_init(&server->fs_lock);
	INIT_LIST_HEAD(&server->cb_promises);
	spin_lock_init(&server->cb_lock);

	for (loop=0; loop<AFS_SERVER_CONN_LIST_SIZE; loop++)
		server->fs_conn_cnt[loop] = 4;

	memcpy(&server->addr,addr,sizeof(struct in_addr));
	server->addr.s_addr = addr->s_addr;

	afs_timer_init(&server->timeout,&afs_server_timer_ops);

	/* add to the cell */
	write_lock(&cell->sv_lock);

	/* check the active list */
	list_for_each(_p,&cell->sv_list) {
		active = list_entry(_p,afs_server_t,link);

		if (active->addr.s_addr==addr->s_addr)
			goto use_active_server;
	}

	/* check the inactive list */
	spin_lock(&cell->sv_gylock);
	list_for_each(_p,&cell->sv_graveyard) {
		zombie = list_entry(_p,afs_server_t,link);

		if (zombie->addr.s_addr==addr->s_addr)
			goto resurrect_server;
	}
	spin_unlock(&cell->sv_gylock);

	afs_get_cell(cell);
	server->cell = cell;
	list_add_tail(&server->link,&cell->sv_list);

	write_unlock(&cell->sv_lock);

	*_server = server;
	_leave(" = 0 (%p)",server);
	return 0;

	/* found a matching active server */
 use_active_server:
	_debug("active server");
	afs_get_server(active);
	write_unlock(&cell->sv_lock);

	kfree(server);

	*_server = active;
	_leave(" = 0 (%p)",active);
	return 0;

	/* found a matching server in the graveyard, so resurrect it and dispose of the new rec */
 resurrect_server:
	_debug("resurrecting server");

	list_del(&zombie->link);
	list_add_tail(&zombie->link,&cell->sv_list);
	afs_get_server(zombie);
	afs_kafstimod_del_timer(&zombie->timeout);
	spin_unlock(&cell->sv_gylock);
	write_unlock(&cell->sv_lock);

	kfree(server);

	*_server = zombie;
	_leave(" = 0 (%p)",zombie);
	return 0;

} /* end afs_server_lookup() */

/*****************************************************************************/
/*
 * destroy a server record
 * - removes from the cell list
 */
void afs_put_server(afs_server_t *server)
{
	afs_cell_t *cell;

	_enter("%p",server);

	cell = server->cell;

	/* sanity check */
	if (atomic_read(&server->usage)<=0)
		BUG();

	/* to prevent a race, the decrement and the dequeue must be effectively atomic */
	write_lock(&cell->sv_lock);

	if (likely(!atomic_dec_and_test(&server->usage))) {
		write_unlock(&cell->sv_lock);
		_leave("");
		return;
	}

	spin_lock(&cell->sv_gylock);
	list_del(&server->link);
	list_add_tail(&server->link,&cell->sv_graveyard);

	/* time out in 10 secs */
	afs_kafstimod_add_timer(&server->timeout,10*HZ);

	spin_unlock(&cell->sv_gylock);
	write_unlock(&cell->sv_lock);

	_leave(" [killed]");
} /* end afs_put_server() */

/*****************************************************************************/
/*
 * timeout server record
 * - removes from the cell's graveyard if the usage count is zero
 */
void afs_server_do_timeout(afs_server_t *server)
{
	struct rxrpc_peer *peer;
	afs_cell_t *cell;
	int loop;

	_enter("%p",server);

	cell = server->cell;

	if (atomic_read(&server->usage)<0) BUG();

	/* remove from graveyard if still dead */
	spin_lock(&cell->vl_gylock);
	if (atomic_read(&server->usage)==0)
		list_del_init(&server->link);
	else
		server = NULL;
	spin_unlock(&cell->vl_gylock);

	if (!server) {
		_leave("");
		return; /* resurrected */
	}

	/* we can now destroy it properly */
	afs_put_cell(cell);

	/* uncross-point the structs under a global lock */
	spin_lock(&afs_server_peer_lock);
	peer = server->peer;
	if (peer) {
		server->peer = NULL;
		peer->user = NULL;
	}
	spin_unlock(&afs_server_peer_lock);

	/* finish cleaning up the server */
	for (loop=AFS_SERVER_CONN_LIST_SIZE-1; loop>=0; loop--)
		if (server->fs_conn[loop])
			rxrpc_put_connection(server->fs_conn[loop]);

	if (server->vlserver)
		rxrpc_put_connection(server->vlserver);

	kfree(server);

	_leave(" [destroyed]");
} /* end afs_server_do_timeout() */

/*****************************************************************************/
/*
 * get a callslot on a connection to the fileserver on the specified server
 */
int afs_server_request_callslot(afs_server_t *server, struct afs_server_callslot *callslot)
{
	struct afs_server_callslot *pcallslot;
	struct rxrpc_connection *conn;
	int nconn, ret;

	_enter("%p,",server);

	INIT_LIST_HEAD(&callslot->link);
	callslot->task = current;
	callslot->conn = NULL;
	callslot->nconn = -1;
	callslot->ready = 0;

	ret = 0;
	conn = NULL;

	/* get hold of a callslot first */
	spin_lock(&server->fs_lock);

	/* resurrect the server if it's death timeout has expired */
	if (server->fs_state) {
		if (time_before(jiffies,server->fs_dead_jif)) {
			ret = server->fs_state;
			spin_unlock(&server->fs_lock);
			_leave(" = %d [still dead]",ret);
			return ret;
		}

		server->fs_state = 0;
	}

	/* try and find a connection that has spare callslots */
	for (nconn=0; nconn<AFS_SERVER_CONN_LIST_SIZE; nconn++) {
		if (server->fs_conn_cnt[nconn]>0) {
			server->fs_conn_cnt[nconn]--;
			spin_unlock(&server->fs_lock);
			callslot->nconn = nconn;
			goto obtained_slot;
		}
	}

	/* none were available - wait interruptibly for one to become available */
	set_current_state(TASK_INTERRUPTIBLE);
	list_add_tail(&callslot->link,&server->fs_callq);
	spin_unlock(&server->fs_lock);

	while (!callslot->ready && !signal_pending(current)) {
		schedule();
		set_current_state(TASK_INTERRUPTIBLE);
	}

	set_current_state(TASK_RUNNING);

	/* even if we were interrupted we may still be queued */
	if (!callslot->ready) {
		spin_lock(&server->fs_lock);
		list_del_init(&callslot->link);
		spin_unlock(&server->fs_lock);
	}

	nconn = callslot->nconn;

	/* if interrupted, we must release any slot we also got before returning an error */
	if (signal_pending(current)) {
		ret = -EINTR;
		goto error_release;
	}

	/* if we were woken up with an error, then pass that error back to the called */
	if (nconn<0) {
		_leave(" = %d",callslot->errno);
		return callslot->errno;
	}

	/* were we given a connection directly? */
	if (callslot->conn) {
		/* yes - use it */
		_leave(" = 0 (nc=%d)",nconn);
		return 0;
	}

	/* got a callslot, but no connection */
 obtained_slot:

	/* need to get hold of the RxRPC connection */
	down_write(&server->sem);

	/* quick check to see if there's an outstanding error */
	ret = server->fs_state;
	if (ret)
		goto error_release_upw;

	if (server->fs_conn[nconn]) {
		/* reuse an existing connection */
		rxrpc_get_connection(server->fs_conn[nconn]);
		callslot->conn = server->fs_conn[nconn];
	}
	else {
		/* create a new connection */
		ret = rxrpc_create_connection(afs_transport,
					      htons(7000),
					      server->addr.s_addr,
					      FS_SERVICE_ID,
					      NULL,
					      &server->fs_conn[nconn]);

		if (ret<0)
			goto error_release_upw;

		callslot->conn = server->fs_conn[0];
		rxrpc_get_connection(callslot->conn);
	}

	up_write(&server->sem);

 	_leave(" = 0");
	return 0;

	/* handle an error occurring */
 error_release_upw:
	up_write(&server->sem);

 error_release:
	/* either release the callslot or pass it along to another deserving task */
	spin_lock(&server->fs_lock);

	if (nconn<0) {
		/* no callslot allocated */
	}
	else if (list_empty(&server->fs_callq)) {
		/* no one waiting */
		server->fs_conn_cnt[nconn]++;
		spin_unlock(&server->fs_lock);
	}
	else {
		/* someone's waiting - dequeue them and wake them up */
		pcallslot = list_entry(server->fs_callq.next,struct afs_server_callslot,link);
		list_del_init(&pcallslot->link);

		pcallslot->errno = server->fs_state;
		if (!pcallslot->errno) {
			/* pass them out callslot details */
			callslot->conn = xchg(&pcallslot->conn,callslot->conn);
			pcallslot->nconn = nconn;
			callslot->nconn = nconn = -1;
		}
		pcallslot->ready = 1;
		wake_up_process(pcallslot->task);
		spin_unlock(&server->fs_lock);
	}

	if (callslot->conn) rxrpc_put_connection(callslot->conn);
	callslot->conn = NULL;

	_leave(" = %d",ret);
	return ret;

} /* end afs_server_request_callslot() */

/*****************************************************************************/
/*
 * release a callslot back to the server
 * - transfers the RxRPC connection to the next pending callslot if possible
 */
void afs_server_release_callslot(afs_server_t *server, struct afs_server_callslot *callslot)
{
	struct afs_server_callslot *pcallslot;

	_enter("{ad=%08x,cnt=%u},{%d}",
	       ntohl(server->addr.s_addr),
	       server->fs_conn_cnt[callslot->nconn],
	       callslot->nconn);

	if (callslot->nconn<0) BUG();

	spin_lock(&server->fs_lock);

	if (list_empty(&server->fs_callq)) {
		/* no one waiting */
		server->fs_conn_cnt[callslot->nconn]++;
		spin_unlock(&server->fs_lock);
	}
	else {
		/* someone's waiting - dequeue them and wake them up */
		pcallslot = list_entry(server->fs_callq.next,struct afs_server_callslot,link);
		list_del_init(&pcallslot->link);

		pcallslot->errno = server->fs_state;
		if (!pcallslot->errno) {
			/* pass them out callslot details */
			callslot->conn = xchg(&pcallslot->conn,callslot->conn);
			pcallslot->nconn = callslot->nconn;
			callslot->nconn = -1;
		}

		pcallslot->ready = 1;
		wake_up_process(pcallslot->task);
		spin_unlock(&server->fs_lock);
	}

	if (callslot->conn) rxrpc_put_connection(callslot->conn);

	_leave("");
} /* end afs_server_release_callslot() */

/*****************************************************************************/
/*
 * get a handle to a connection to the vlserver (volume location) on the specified server
 */
int afs_server_get_vlconn(afs_server_t *server, struct rxrpc_connection **_conn)
{
	struct rxrpc_connection *conn;
	int ret;

	_enter("%p,",server);

	ret = 0;
	conn = NULL;
	down_read(&server->sem);

	if (server->vlserver) {
		/* reuse an existing connection */
		rxrpc_get_connection(server->vlserver);
		conn = server->vlserver;
		up_read(&server->sem);
	}
	else {
		/* create a new connection */
		up_read(&server->sem);
		down_write(&server->sem);
		if (!server->vlserver) {
			ret = rxrpc_create_connection(afs_transport,
						      htons(7003),
						      server->addr.s_addr,
						      VL_SERVICE_ID,
						      NULL,
						      &server->vlserver);
		}
		if (ret==0) {
			rxrpc_get_connection(server->vlserver);
			conn = server->vlserver;
		}
		up_write(&server->sem);
	}

	*_conn = conn;
	_leave(" = %d",ret);
	return ret;
} /* end afs_server_get_vlconn() */
