fts_index_timeout setting that determines how long we'll wait for
FTS indexing to finish before aborting. Not sure what (if anything) is still
missing from this patch.

diff -r 780a8f4544ec src/indexer/indexer-client.c
--- a/src/indexer/indexer-client.c	Sun May 03 16:26:46 2015 +0300
+++ b/src/indexer/indexer-client.c	Sun May 03 16:53:14 2015 +0300
@@ -67,9 +67,9 @@
 			     const char *const *args, const char **error_r)
 {
 	struct indexer_client_request *ctx = NULL;
-	unsigned int tag, max_recent_msgs;
+	unsigned int tag, max_recent_msgs = 0, timeout_secs = 0;
 
-	/* <tag> <user> <mailbox> [<max_recent_msgs>] */
+	/* <tag> <user> <mailbox> [<max_recent_msgs> [<timeout secs>]] */
 	if (str_array_length(args) < 3) {
 		*error_r = "Wrong parameter count";
 		return -1;
@@ -78,11 +78,17 @@
 		*error_r = "Invalid tag";
 		return -1;
 	}
-	if (args[3] == NULL)
-		max_recent_msgs = 0;
-	else if (str_to_uint(args[3], &max_recent_msgs) < 0) {
-		*error_r = "Invalid max_recent_msgs";
-		return -1;
+	if (args[3] != NULL) {
+		if (str_to_uint(args[3], &max_recent_msgs) < 0) {
+			*error_r = "Invalid max_recent_msgs";
+			return -1;
+		}
+		if (args[4] != NULL) {
+			if (str_to_uint(args[4], &timeout_secs) < 0) {
+				*error_r = "Invalid timeout_secs";
+				return -1;
+			}
+		}
 	}
 
 	if (tag != 0) {
@@ -93,7 +99,7 @@
 	}
 
 	indexer_queue_append(client->queue, append, args[1], args[2],
-			     max_recent_msgs, ctx);
+			     max_recent_msgs, timeout_secs, ctx);
 	o_stream_nsend_str(client->output, t_strdup_printf("%u\tOK\n", tag));
 	return 0;
 }
diff -r 780a8f4544ec src/indexer/indexer-queue.c
--- a/src/indexer/indexer-queue.c	Sun May 03 16:26:46 2015 +0300
+++ b/src/indexer/indexer-queue.c	Sun May 03 16:53:14 2015 +0300
@@ -68,30 +68,34 @@
 	return hash_table_lookup(queue->requests, &lookup_request);
 }
 
-static void request_add_context(struct indexer_request *request, void *context)
+static void request_add_context(struct indexer_request *request, void *context,
+				unsigned int timeout_secs)
 {
 	unsigned int count = 0;
 
 	if (context == NULL)
 		return;
 
-	if (request->contexts == NULL) {
-		request->contexts = i_new(void *, 2);
+	if (request->_contexts == NULL) {
+		request->_contexts = i_new(struct indexer_request_context, 2);
 	} else {
-		for (; request->contexts[count] != NULL; count++) ;
+		for (; request->_contexts[count].context != NULL; count++) ;
 
-		request->contexts =
-			i_realloc(request->contexts,
-				  sizeof(*request->contexts) * (count + 1),
-				  sizeof(*request->contexts) * (count + 2));
+		request->_contexts =
+			i_realloc(request->_contexts,
+				  sizeof(*request->_contexts) * (count + 1),
+				  sizeof(*request->_contexts) * (count + 2));
 	}
-	request->contexts[count] = context;
+	request->_contexts[count].context = context;
+	request->_contexts[count].timeout =
+		timeout_secs == 0 ? 0 : ioloop_time + timeout_secs;
 }
 
 static struct indexer_request *
 indexer_queue_append_request(struct indexer_queue *queue, bool append,
 			     const char *username, const char *mailbox,
-			     unsigned int max_recent_msgs, void *context)
+			     unsigned int max_recent_msgs,
+			     unsigned int timeout_secs, void *context)
 {
 	struct indexer_request *request;
 
@@ -101,12 +105,12 @@
 		request->username = i_strdup(username);
 		request->mailbox = i_strdup(mailbox);
 		request->max_recent_msgs = max_recent_msgs;
-		request_add_context(request, context);
+		request_add_context(request, context, timeout_secs);
 		hash_table_insert(queue->requests, request, request);
 	} else {
 		if (request->max_recent_msgs > max_recent_msgs)
 			request->max_recent_msgs = max_recent_msgs;
-		request_add_context(request, context);
+		request_add_context(request, context, timeout_secs);
 		if (append) {
 			/* keep the request in its old position */
 			return request;
@@ -131,12 +135,14 @@
 
 void indexer_queue_append(struct indexer_queue *queue, bool append,
 			  const char *username, const char *mailbox,
-			  unsigned int max_recent_msgs, void *context)
+			  unsigned int max_recent_msgs,
+			  unsigned int timeout_secs, void *context)
 {
 	struct indexer_request *request;
 
 	request = indexer_queue_append_request(queue, append, username, mailbox,
-					       max_recent_msgs, context);
+					       max_recent_msgs, timeout_secs,
+					       context);
 	request->index = TRUE;
 	indexer_queue_append_finish(queue);
 }
@@ -148,7 +154,7 @@
 	struct indexer_request *request;
 
 	request = indexer_queue_append_request(queue, TRUE, username, mailbox,
-					       0, context);
+					       0, 0, context);
 	request->optimize = TRUE;
 	indexer_queue_append_finish(queue);
 }
@@ -175,9 +181,9 @@
 {
 	unsigned int i;
 
-	if (request->contexts != NULL) {
-		for (i = 0; request->contexts[i] != NULL; i++)
-			queue->callback(percentage, request->contexts[i]);
+	if (request->_contexts != NULL) {
+		for (i = 0; request->_contexts[i].context != NULL; i++)
+			queue->callback(percentage, request->contexts[i].context);
 	}
 }
 
diff -r 780a8f4544ec src/indexer/indexer-queue.h
--- a/src/indexer/indexer-queue.h	Sun May 03 16:26:46 2015 +0300
+++ b/src/indexer/indexer-queue.h	Sun May 03 16:53:14 2015 +0300
@@ -3,6 +3,13 @@
 
 #include "indexer.h"
 
+struct indexer_request_context {
+	void *context;
+	/* timestamp when this request should just timeout instead of waiting
+	   for more. 0 = never. */
+	time_t timeout;
+};
+
 struct indexer_request {
 	struct indexer_request *prev, *next;
 
@@ -15,7 +22,7 @@
 	/* optimize this mailbox */
 	unsigned int optimize:1;
 
-	void **contexts;
+	struct indexer_request_context *_contexts;
 };
 
 struct indexer_queue *indexer_queue_init(indexer_status_callback_t *callback);
@@ -27,7 +34,8 @@
 	
 void indexer_queue_append(struct indexer_queue *queue, bool append,
 			  const char *username, const char *mailbox,
-			  unsigned int max_recent_msgs, void *context);
+			  unsigned int max_recent_msgs,
+			  unsigned int timeout_secs, void *context);
 void indexer_queue_append_optimize(struct indexer_queue *queue,
 				   const char *username, const char *mailbox,
 				   void *context);
diff -r 780a8f4544ec src/indexer/indexer.c
--- a/src/indexer/indexer.c	Sun May 03 16:26:46 2015 +0300
+++ b/src/indexer/indexer.c	Sun May 03 16:53:14 2015 +0300
@@ -10,6 +10,12 @@
 #include "worker-pool.h"
 #include "worker-connection.h"
 
+/* if indexing is taking a long time and another request is sent for the
+   same user, we'll attempt to return failure immediately for the request
+   if it looks like it's going to timeout anyway. do this only for requests
+   that have been running for at least this many seconds. */
+#define INDEXER_TIMEOUT_CHECK_MIN_ELAPSED_TIME 30
+
 struct worker_request {
 	struct worker_connection *conn;
 	struct indexer_request *request;
@@ -53,6 +59,28 @@
 	worker_connection_request(conn, request, wrequest);
 }
 
+static bool worker_request_will_timeout(struct worker_connection *conn,
+					struct indexer_request *request)
+{
+	time_t start_time;
+	unsigned int percentage, elapsed_secs, total_estimated_secs;
+	unsigned int estimated_secs_left;
+
+	if (request->timeout == 0)
+		return FALSE;
+
+	if (!worker_connection_last_cmd(conn, &start_time, &percentage))
+		i_unreached();
+	elapsed_secs = ioloop_time - start_time;
+	if (elapsed_secs < INDEXER_TIMEOUT_CHECK_MIN_ELAPSED_TIME)
+		return FALSE;
+	total_estimated_secs = elapsed_secs * 100 / I_MAX(percentage, 1);
+	estimated_secs_left = total_estimated_secs - elapsed_secs;
+
+	return ioloop_time > request->timeout ||
+		request->timeout - ioloop_time < estimated_secs_left;
+}
+
 static void queue_handle_existing_user_requests(struct indexer_queue *queue)
 {
 	struct worker_connection *conn;
@@ -67,7 +95,13 @@
 		indexer_queue_request_remove(queue);
 		/* there is already a worker handling this user.
 		   it must be the one doing the indexing. */
-		worker_send_request(conn, request);
+		if (!worker_request_will_timeout(conn, request))
+			worker_send_request(conn, request);
+		else {
+			/* this request is going to timeout.
+			   fail it immediately. */
+			indexer_queue_request_finish(queue, &request, FALSE);
+		}
 	}
 }
 
@@ -83,8 +117,14 @@
 		return;
 
 	/* okay, we have a request for a new user. */
-	if (!worker_pool_get_connection(worker_pool, &conn))
+	if (!worker_pool_get_connection(worker_pool, &conn)) {
+		if (request->timeout > 0 && ioloop_time > request->timeout) {
+			/* the request timed out already, fail it */
+			indexer_queue_request_remove(queue);
+			indexer_queue_request_finish(queue, &request, FALSE);
+		}
 		return;
+	}
 
 	indexer_queue_request_remove(queue);
 	worker_send_request(conn, request);
diff -r 780a8f4544ec src/indexer/worker-connection.c
--- a/src/indexer/worker-connection.c	Sun May 03 16:26:46 2015 +0300
+++ b/src/indexer/worker-connection.c	Sun May 03 16:53:14 2015 +0300
@@ -35,6 +35,9 @@
 	ARRAY(void *) request_contexts;
 	struct aqueue *request_queue;
 
+	time_t last_cmd_started;
+	unsigned int last_percentage;
+
 	unsigned int process_limit;
 	unsigned int version_received:1;
 };
@@ -140,6 +143,7 @@
 			i_free_and_null(conn->request_username);
 	}
 
+	conn->last_percentage = percentage;
 	conn->callback(percentage, context);
 	return 0;
 }
@@ -218,6 +222,17 @@
 	return TRUE;
 }
 
+bool worker_connection_last_cmd(struct worker_connection *conn,
+				time_t *start_time_r,
+				unsigned int *percentage_r)
+{
+	if (aqueue_count(conn->request_queue) == 0)
+		return FALSE;
+	*start_time_r = conn->last_cmd_started;
+	*percentage_r = conn->last_percentage;
+	return TRUE;
+}
+
 void worker_connection_request(struct worker_connection *conn,
 			       const struct indexer_request *request,
 			       void *context)
@@ -226,9 +241,13 @@
 	i_assert(context != NULL);
 	i_assert(request->index || request->optimize);
 
-	if (conn->request_username == NULL)
+	if (conn->request_username == NULL) {
+		i_assert(aqueue_count(conn->request_queue) == 0);
+		conn->last_cmd_started = ioloop_time;
+		conn->last_percentage = 0;
 		conn->request_username = i_strdup(request->username);
-	else {
+	} else {
+		i_assert(aqueue_count(conn->request_queue) > 0);
 		i_assert(strcmp(conn->request_username,
 				request->username) == 0);
 	}
diff -r 780a8f4544ec src/indexer/worker-connection.h
--- a/src/indexer/worker-connection.h	Sun May 03 16:26:46 2015 +0300
+++ b/src/indexer/worker-connection.h	Sun May 03 16:53:14 2015 +0300
@@ -19,6 +19,11 @@
    finished yet. */
 bool worker_connection_get_process_limit(struct worker_connection *conn,
 					 unsigned int *limit_r);
+/* Get information about the worker's currently running command.
+   Returns TRUE if values are returned, FALSE if no command is running. */
+bool worker_connection_last_cmd(struct worker_connection *conn,
+				time_t *start_time_r,
+				unsigned int *percentage_r);
 
 /* Send a new indexing request for username+mailbox. The status callback is
    called as necessary with the given context. Requests can be queued, but
diff -r 780a8f4544ec src/plugins/fts/fts-indexer.c
--- a/src/plugins/fts/fts-indexer.c	Sun May 03 16:26:46 2015 +0300
+++ b/src/plugins/fts/fts-indexer.c	Sun May 03 16:53:14 2015 +0300
@@ -96,6 +96,7 @@
 	struct mailbox_status status;
 	uint32_t last_uid, seq1, seq2;
 	const char *path, *cmd, *value, *error;
+	unsigned int timeout_secs = 0;
 	int fd;
 
 	if (fts_backend_get_last_uid(backend, box, &last_uid) < 0)
@@ -113,9 +114,15 @@
 		return 0;
 	}
 
-	cmd = t_strdup_printf("PREPEND\t1\t%s\t%s\n",
+	value = mail_user_plugin_getenv(box->storage->user, "fts_index_timeout");
+	if (value != NULL) {
+		if (settings_get_time(value, &timeout_secs, &error) < 0)
+			i_error("Invalid fts_index_timeout setting: %s", error);
+	}
+
+	cmd = t_strdup_printf("PREPEND\t1\t%s\t%s0\t%u\n",
 			      str_tabescape(box->storage->user->username),
-			      str_tabescape(box->vname));
+			      str_tabescape(box->vname), timeout_secs);
 	fd = fts_indexer_cmd(box->storage->user, cmd, &path);
 	if (fd == -1)
 		return -1;
@@ -127,13 +134,7 @@
 	ctx->fd = fd;
 	ctx->input = i_stream_create_fd(fd, 128, FALSE);
 	ctx->search_start_time = ioloop_timeval;
-
-	value = mail_user_plugin_getenv(box->storage->user, "fts_index_timeout");
-	if (value != NULL) {
-		if (settings_get_time(value, &ctx->timeout_secs, &error) < 0)
-			i_error("Invalid fts_index_timeout setting: %s", error);
-	}
-
+	ctx->timeout_secs = timeout_secs;
 
 	*ctx_r = ctx;
 	return 1;
