+ /* don't re-write updates in replay mode */
+ if (sock != NULL)
+ journal_write("update", orig_buf);
+
+ while (buffer_size > 0)
+ {
+ char *value;
+ time_t stamp;
+ char *eostamp;
+
+ status = buffer_get_field (&buffer, &buffer_size, &value);
+ if (status != 0)
+ {
+ RRDD_LOG (LOG_INFO, "handle_request_update: Error reading field.");
+ break;
+ }
+
+ /* make sure update time is always moving forward */
+ stamp = strtol(value, &eostamp, 10);
+ if (eostamp == value || eostamp == NULL || *eostamp != ':')
+ {
+ pthread_mutex_unlock(&cache_lock);
+ return send_response(sock, RESP_ERR,
+ "Cannot find timestamp in '%s'!\n", value);
+ }
+ else if (stamp <= ci->last_update_stamp)
+ {
+ pthread_mutex_unlock(&cache_lock);
+ return send_response(sock, RESP_ERR,
+ "illegal attempt to update using time %ld when last"
+ " update time is %ld (minimum one second step)\n",
+ stamp, ci->last_update_stamp);
+ }
+ else
+ ci->last_update_stamp = stamp;
+
+ if (!rrd_add_strdup(&ci->values, &ci->values_num, value))
+ {
+ RRDD_LOG (LOG_ERR, "handle_request_update: rrd_add_strdup failed.");
+ continue;
+ }
+
+ values_num++;
+ }
+
+ if (((now - ci->last_flush_time) >= config_write_interval)
+ && ((ci->flags & CI_FLAGS_IN_QUEUE) == 0)
+ && (ci->values_num > 0))
+ {
+ enqueue_cache_item (ci, TAIL);
+ }
+
+ pthread_mutex_unlock (&cache_lock);
+
+ if (values_num < 1)
+ return send_response(sock, RESP_ERR, "No values updated.\n");
+ else
+ return send_response(sock, RESP_OK,
+ "errors, enqueued %i value(s).\n", values_num);
+
+ /* NOTREACHED */
+ assert(1==0);
+
+} /* }}} int handle_request_update */
+
+/* we came across a "WROTE" entry during journal replay.
+ * throw away any values that we have accumulated for this file
+ */
+static int handle_request_wrote (HANDLER_PROTO) /* {{{ */
+{
+ cache_item_t *ci;
+ const char *file = buffer;
+
+ pthread_mutex_lock(&cache_lock);
+
+ ci = g_tree_lookup(cache_tree, file);
+ if (ci == NULL)
+ {
+ pthread_mutex_unlock(&cache_lock);
+ return (0);
+ }
+
+ if (ci->values)
+ rrd_free_ptrs((void ***) &ci->values, &ci->values_num);
+
+ wipe_ci_values(ci, now);
+ remove_from_queue(ci);
+
+ pthread_mutex_unlock(&cache_lock);
+ return (0);
+} /* }}} int handle_request_wrote */
+
+/* start "BATCH" processing */
+static int batch_start (HANDLER_PROTO) /* {{{ */
+{
+ int status;
+ if (sock->batch_start)
+ return send_response(sock, RESP_ERR, "Already in BATCH\n");
+
+ status = send_response(sock, RESP_OK,
+ "Go ahead. End with dot '.' on its own line.\n");
+ sock->batch_start = time(NULL);
+ sock->batch_cmd = 0;
+
+ return status;
+} /* }}} static int batch_start */
+
+/* finish "BATCH" processing and return results to the client */
+static int batch_done (HANDLER_PROTO) /* {{{ */
+{
+ assert(sock->batch_start);
+ sock->batch_start = 0;
+ sock->batch_cmd = 0;
+ return send_response(sock, RESP_OK, "errors\n");
+} /* }}} static int batch_done */
+
+static int handle_request_quit (HANDLER_PROTO) /* {{{ */
+{
+ return -1;
+} /* }}} static int handle_request_quit */
+
+struct command COMMANDS[] = {
+ {
+ "UPDATE",
+ handle_request_update,
+ PRIV_HIGH,
+ CMD_CONTEXT_ANY,
+ "UPDATE <filename> <values> [<values> ...]\n"
+ ,
+ "Adds the given file to the internal cache if it is not yet known and\n"
+ "appends the given value(s) to the entry. See the rrdcached(1) manpage\n"
+ "for details.\n"
+ "\n"
+ "Each <values> has the following form:\n"
+ " <values> = <time>:<value>[:<value>[...]]\n"
+ "See the rrdupdate(1) manpage for details.\n"
+ },
+ {
+ "WROTE",
+ handle_request_wrote,
+ PRIV_HIGH,
+ CMD_CONTEXT_JOURNAL,
+ NULL,
+ NULL
+ },
+ {
+ "FLUSH",
+ handle_request_flush,
+ PRIV_LOW,
+ CMD_CONTEXT_CLIENT | CMD_CONTEXT_BATCH,
+ "FLUSH <filename>\n"
+ ,
+ "Adds the given filename to the head of the update queue and returns\n"
+ "after it has been dequeued.\n"
+ },
+ {
+ "FLUSHALL",
+ handle_request_flushall,
+ PRIV_HIGH,
+ CMD_CONTEXT_CLIENT,
+ "FLUSHALL\n"
+ ,
+ "Triggers writing of all pending updates. Returns immediately.\n"
+ },
+ {
+ "PENDING",
+ handle_request_pending,
+ PRIV_HIGH,
+ CMD_CONTEXT_CLIENT,
+ "PENDING <filename>\n"
+ ,
+ "Shows any 'pending' updates for a file, in order.\n"
+ "The updates shown have not yet been written to the underlying RRD file.\n"
+ },
+ {
+ "FORGET",
+ handle_request_forget,
+ PRIV_HIGH,
+ CMD_CONTEXT_ANY,
+ "FORGET <filename>\n"
+ ,
+ "Removes the file completely from the cache.\n"
+ "Any pending updates for the file will be lost.\n"
+ },
+ {
+ "QUEUE",
+ handle_request_queue,
+ PRIV_LOW,
+ CMD_CONTEXT_CLIENT,
+ "QUEUE\n"
+ ,
+ "Shows all files in the output queue.\n"
+ "The output is zero or more lines in the following format:\n"
+ "(where <num_vals> is the number of values to be written)\n"
+ "\n"
+ "<num_vals> <filename>\n"
+ },
+ {
+ "STATS",
+ handle_request_stats,
+ PRIV_LOW,
+ CMD_CONTEXT_CLIENT,
+ "STATS\n"
+ ,
+ "Returns some performance counters, see the rrdcached(1) manpage for\n"
+ "a description of the values.\n"
+ },
+ {
+ "HELP",
+ handle_request_help,
+ PRIV_LOW,
+ CMD_CONTEXT_CLIENT,
+ "HELP [<command>]\n",
+ NULL, /* special! */
+ },
+ {
+ "BATCH",
+ batch_start,
+ PRIV_LOW,
+ CMD_CONTEXT_CLIENT,
+ "BATCH\n"
+ ,
+ "The 'BATCH' command permits the client to initiate a bulk load\n"
+ " of commands to rrdcached.\n"
+ "\n"
+ "Usage:\n"
+ "\n"
+ " client: BATCH\n"
+ " server: 0 Go ahead. End with dot '.' on its own line.\n"
+ " client: command #1\n"
+ " client: command #2\n"
+ " client: ... and so on\n"
+ " client: .\n"
+ " server: 2 errors\n"
+ " server: 7 message for command #7\n"
+ " server: 9 message for command #9\n"
+ "\n"
+ "For more information, consult the rrdcached(1) documentation.\n"
+ },
+ {
+ ".", /* BATCH terminator */
+ batch_done,
+ PRIV_LOW,
+ CMD_CONTEXT_BATCH,
+ NULL,
+ NULL
+ },
+ {
+ "QUIT",
+ handle_request_quit,
+ PRIV_LOW,
+ CMD_CONTEXT_CLIENT | CMD_CONTEXT_BATCH,
+ "QUIT\n"
+ ,
+ "Disconnect from rrdcached.\n"
+ },
+ {NULL,NULL,0,0,NULL,NULL} /* LAST ENTRY */
+};
+
+static struct command *find_command(char *cmd)
+{
+ struct command *c = COMMANDS;
+
+ while (c->cmd != NULL)
+ {
+ if (strcasecmp(cmd, c->cmd) == 0)
+ break;
+ c++;
+ }
+
+ if (c->cmd == NULL)
+ return NULL;
+ else
+ return c;
+}
+
+/* check whether commands are received in the expected context */
+static int command_check_context(listen_socket_t *sock, struct command *cmd)
+{
+ if (sock == NULL)
+ return (cmd->context & CMD_CONTEXT_JOURNAL);
+ else if (sock->batch_start)
+ return (cmd->context & CMD_CONTEXT_BATCH);
+ else
+ return (cmd->context & CMD_CONTEXT_CLIENT);
+
+ /* NOTREACHED */
+ assert(1==0);
+}
+
+static int handle_request_help (HANDLER_PROTO) /* {{{ */
+{
+ int status;
+ char *cmd_str;
+ char *resp_txt;
+ struct command *help = NULL;
+
+ status = buffer_get_field (&buffer, &buffer_size, &cmd_str);
+ if (status == 0)
+ help = find_command(cmd_str);
+
+ if (help && (help->syntax || help->help))
+ {
+ char tmp[CMD_MAX];
+
+ snprintf(tmp, sizeof(tmp)-1, "Help for %s\n", help->cmd);
+ resp_txt = tmp;
+
+ if (help->syntax)
+ add_response_info(sock, "Usage: %s\n", help->syntax);
+
+ if (help->help)
+ add_response_info(sock, "%s\n", help->help);
+ }
+ else
+ {
+ help = COMMANDS;
+ resp_txt = "Command overview\n";
+
+ while (help->cmd)
+ {
+ if (help->syntax)
+ add_response_info(sock, "%s", help->syntax);
+ help++;
+ }
+ }
+
+ return send_response(sock, RESP_OK, resp_txt);
+} /* }}} int handle_request_help */
+
+/* if sock==NULL, we are in journal replay mode */
+static int handle_request (DISPATCH_PROTO) /* {{{ */
+{
+ char *buffer_ptr = buffer;
+ char *cmd_str = NULL;
+ struct command *cmd = NULL;
+ int status;
+
+ assert (buffer[buffer_size - 1] == '\0');
+
+ status = buffer_get_field (&buffer_ptr, &buffer_size, &cmd_str);
+ if (status != 0)
+ {
+ RRDD_LOG (LOG_INFO, "handle_request: Unable parse command.");
+ return (-1);
+ }
+
+ if (sock != NULL && sock->batch_start)
+ sock->batch_cmd++;
+
+ cmd = find_command(cmd_str);
+ if (!cmd)
+ return send_response(sock, RESP_ERR, "Unknown command: %s\n", cmd_str);
+
+ status = has_privilege(sock, cmd->min_priv);
+ if (status <= 0)
+ return status;
+
+ if (!command_check_context(sock, cmd))
+ return send_response(sock, RESP_ERR, "Can't use '%s' here.\n", cmd_str);
+
+ return cmd->handler(cmd, sock, now, buffer_ptr, buffer_size);
+} /* }}} int handle_request */
+
+/* MUST NOT hold journal_lock before calling this */
+static void journal_rotate(void) /* {{{ */
+{
+ FILE *old_fh = NULL;
+ int new_fd;
+
+ if (journal_cur == NULL || journal_old == NULL)
+ return;
+
+ pthread_mutex_lock(&journal_lock);
+
+ /* we rotate this way (rename before close) so that the we can release
+ * the journal lock as fast as possible. Journal writes to the new
+ * journal can proceed immediately after the new file is opened. The
+ * fclose can then block without affecting new updates.
+ */
+ if (journal_fh != NULL)
+ {
+ old_fh = journal_fh;
+ journal_fh = NULL;
+ rename(journal_cur, journal_old);
+ ++stats_journal_rotate;
+ }
+
+ new_fd = open(journal_cur, O_WRONLY|O_CREAT|O_APPEND,
+ S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH);
+ if (new_fd >= 0)
+ {
+ journal_fh = fdopen(new_fd, "a");
+ if (journal_fh == NULL)
+ close(new_fd);
+ }
+
+ pthread_mutex_unlock(&journal_lock);
+
+ if (old_fh != NULL)
+ fclose(old_fh);
+
+ if (journal_fh == NULL)
+ {
+ RRDD_LOG(LOG_CRIT,
+ "JOURNALING DISABLED: Cannot open journal file '%s' : (%s)",
+ journal_cur, rrd_strerror(errno));
+
+ RRDD_LOG(LOG_ERR,
+ "JOURNALING DISABLED: All values will be flushed at shutdown");
+ config_flush_at_shutdown = 1;
+ }
+
+} /* }}} static void journal_rotate */
+
+static void journal_done(void) /* {{{ */
+{
+ if (journal_cur == NULL)
+ return;
+
+ pthread_mutex_lock(&journal_lock);
+ if (journal_fh != NULL)
+ {
+ fclose(journal_fh);
+ journal_fh = NULL;
+ }
+
+ if (config_flush_at_shutdown)
+ {
+ RRDD_LOG(LOG_INFO, "removing journals");
+ unlink(journal_old);
+ unlink(journal_cur);
+ }
+ else
+ {
+ RRDD_LOG(LOG_INFO, "expedited shutdown; "
+ "journals will be used at next startup");
+ }
+
+ pthread_mutex_unlock(&journal_lock);
+
+} /* }}} static void journal_done */
+
+static int journal_write(char *cmd, char *args) /* {{{ */
+{
+ int chars;
+
+ if (journal_fh == NULL)
+ return 0;
+
+ pthread_mutex_lock(&journal_lock);
+ chars = fprintf(journal_fh, "%s %s\n", cmd, args);
+ pthread_mutex_unlock(&journal_lock);
+
+ if (chars > 0)
+ {
+ pthread_mutex_lock(&stats_lock);
+ stats_journal_bytes += chars;
+ pthread_mutex_unlock(&stats_lock);
+ }
+
+ return chars;
+} /* }}} static int journal_write */
+
+static int journal_replay (const char *file) /* {{{ */
+{
+ FILE *fh;
+ int entry_cnt = 0;
+ int fail_cnt = 0;
+ uint64_t line = 0;
+ char entry[CMD_MAX];
+ time_t now;
+
+ if (file == NULL) return 0;
+
+ {
+ char *reason = "unknown error";
+ int status = 0;
+ struct stat statbuf;
+
+ memset(&statbuf, 0, sizeof(statbuf));
+ if (stat(file, &statbuf) != 0)
+ {
+ if (errno == ENOENT)
+ return 0;
+
+ reason = "stat error";
+ status = errno;
+ }
+ else if (!S_ISREG(statbuf.st_mode))
+ {
+ reason = "not a regular file";
+ status = EPERM;
+ }
+ if (statbuf.st_uid != daemon_uid)
+ {
+ reason = "not owned by daemon user";
+ status = EACCES;
+ }
+ if (statbuf.st_mode & (S_IWGRP|S_IWOTH))
+ {
+ reason = "must not be user/group writable";
+ status = EACCES;
+ }
+
+ if (status != 0)
+ {
+ RRDD_LOG(LOG_ERR, "journal_replay: %s : %s (%s)",
+ file, rrd_strerror(status), reason);
+ return 0;
+ }
+ }
+
+ fh = fopen(file, "r");
+ if (fh == NULL)
+ {
+ if (errno != ENOENT)
+ RRDD_LOG(LOG_ERR, "journal_replay: cannot open journal file: '%s' (%s)",
+ file, rrd_strerror(errno));
+ return 0;
+ }
+ else
+ RRDD_LOG(LOG_NOTICE, "replaying from journal: %s", file);
+
+ now = time(NULL);
+
+ while(!feof(fh))