+ 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 */
+
+static command_t list_of_commands[] = { /* {{{ */
+ {
+ "UPDATE",
+ handle_request_update,
+ 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,
+ CMD_CONTEXT_JOURNAL,
+ NULL,
+ NULL
+ },
+ {
+ "FLUSH",
+ handle_request_flush,
+ 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,
+ CMD_CONTEXT_CLIENT,
+ "FLUSHALL\n"
+ ,
+ "Triggers writing of all pending updates. Returns immediately.\n"
+ },
+ {
+ "PENDING",
+ handle_request_pending,
+ 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,
+ 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,
+ 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,
+ 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,
+ CMD_CONTEXT_CLIENT,
+ "HELP [<command>]\n",
+ NULL, /* special! */
+ },
+ {
+ "BATCH",
+ batch_start,
+ 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,
+ CMD_CONTEXT_BATCH,
+ NULL,
+ NULL
+ },
+ {
+ "QUIT",
+ handle_request_quit,
+ CMD_CONTEXT_CLIENT | CMD_CONTEXT_BATCH,
+ "QUIT\n"
+ ,
+ "Disconnect from rrdcached.\n"
+ }
+}; /* }}} command_t list_of_commands[] */
+static size_t list_of_commands_len = sizeof (list_of_commands)
+ / sizeof (list_of_commands[0]);
+
+static command_t *find_command(char *cmd)
+{
+ size_t i;
+
+ for (i = 0; i < list_of_commands_len; i++)
+ if (strcasecmp(cmd, list_of_commands[i].cmd) == 0)
+ return (&list_of_commands[i]);
+ return NULL;
+}
+
+/* We currently use the index in the `list_of_commands' array as a bit position
+ * in `listen_socket_t.permissions'. This member schould NEVER be accessed from
+ * outside these functions so that switching to a more elegant storage method
+ * is easily possible. */
+static ssize_t find_command_index (const char *cmd) /* {{{ */
+{
+ size_t i;
+
+ for (i = 0; i < list_of_commands_len; i++)
+ if (strcasecmp(cmd, list_of_commands[i].cmd) == 0)
+ return ((ssize_t) i);
+ return (-1);
+} /* }}} ssize_t find_command_index */
+
+static int socket_permission_check (listen_socket_t *sock, /* {{{ */
+ const char *cmd)
+{
+ ssize_t i;
+
+ if (cmd == NULL)
+ return (-1);
+
+ if ((strcasecmp ("QUIT", cmd) == 0)
+ || (strcasecmp ("HELP", cmd) == 0))
+ return (1);
+ else if (strcmp (".", cmd) == 0)
+ cmd = "BATCH";
+
+ i = find_command_index (cmd);
+ if (i < 0)
+ return (-1);
+ assert (i < 32);
+
+ if ((sock->permissions & (1 << i)) != 0)
+ return (1);
+ return (0);
+} /* }}} int socket_permission_check */
+
+static int socket_permission_add (listen_socket_t *sock, /* {{{ */
+ const char *cmd)
+{
+ ssize_t i;
+
+ i = find_command_index (cmd);
+ if (i < 0)
+ return (-1);
+ assert (i < 32);
+
+ sock->permissions |= (1 << i);
+ return (0);
+} /* }}} int socket_permission_add */
+
+/* check whether commands are received in the expected context */
+static int command_check_context(listen_socket_t *sock, command_t *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;
+ command_t *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
+ {
+ size_t i;
+
+ resp_txt = "Command overview\n";
+
+ for (i = 0; i < list_of_commands_len; i++)
+ {
+ if (list_of_commands[i].syntax == NULL)
+ continue;
+ add_response_info (sock, "%s", list_of_commands[i].syntax);
+ }
+ }
+
+ 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;
+ command_t *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);
+
+ if (!socket_permission_check (sock, cmd->cmd))
+ return send_response(sock, RESP_ERR, "Permission denied.\n");
+
+ 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 */
+
+static void journal_set_free (journal_set *js) /* {{{ */
+{
+ if (js == NULL)
+ return;
+
+ rrd_free_ptrs((void ***) &js->files, &js->files_num);
+
+ free(js);
+} /* }}} journal_set_free */
+
+static void journal_set_remove (journal_set *js) /* {{{ */
+{
+ if (js == NULL)
+ return;
+
+ for (uint i=0; i < js->files_num; i++)
+ {
+ RRDD_LOG(LOG_DEBUG, "removing old journal %s", js->files[i]);
+ unlink(js->files[i]);
+ }
+} /* }}} journal_set_remove */
+
+/* close current journal file handle.
+ * MUST hold journal_lock before calling */
+static void journal_close(void) /* {{{ */
+{
+ if (journal_fh != NULL)
+ {
+ if (fclose(journal_fh) != 0)
+ RRDD_LOG(LOG_ERR, "cannot close journal: %s", rrd_strerror(errno));
+ }
+
+ journal_fh = NULL;
+ journal_size = 0;
+} /* }}} journal_close */
+
+/* MUST hold journal_lock before calling */
+static void journal_new_file(void) /* {{{ */
+{
+ struct timeval now;
+ int new_fd;
+ char new_file[PATH_MAX + 1];
+
+ assert(journal_dir != NULL);
+ assert(journal_cur != NULL);
+
+ journal_close();
+
+ gettimeofday(&now, NULL);
+ /* this format assures that the files sort in strcmp() order */
+ snprintf(new_file, PATH_MAX, "%s/%s.%010d.%06d",
+ journal_dir, JOURNAL_BASE, (int)now.tv_sec, (int)now.tv_usec);
+
+ new_fd = open(new_file, O_WRONLY|O_CREAT|O_APPEND,
+ S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH);
+ if (new_fd < 0)
+ goto error;
+
+ journal_fh = fdopen(new_fd, "a");
+ if (journal_fh == NULL)
+ goto error;
+
+ journal_size = ftell(journal_fh);
+ RRDD_LOG(LOG_DEBUG, "started new journal %s", new_file);
+
+ /* record the file in the journal set */
+ rrd_add_strdup(&journal_cur->files, &journal_cur->files_num, new_file);
+
+ return;
+
+error:
+ RRDD_LOG(LOG_CRIT,
+ "JOURNALING DISABLED: Error while trying to create %s : %s",
+ new_file, rrd_strerror(errno));
+ RRDD_LOG(LOG_CRIT,
+ "JOURNALING DISABLED: All values will be flushed at shutdown");
+
+ close(new_fd);
+ config_flush_at_shutdown = 1;
+
+} /* }}} journal_new_file */
+
+/* MUST NOT hold journal_lock before calling this */
+static void journal_rotate(void) /* {{{ */
+{
+ journal_set *old_js = NULL;
+
+ if (journal_dir == NULL)
+ return;
+
+ RRDD_LOG(LOG_DEBUG, "rotating journals");
+
+ pthread_mutex_lock(&stats_lock);
+ ++stats_journal_rotate;
+ pthread_mutex_unlock(&stats_lock);
+
+ pthread_mutex_lock(&journal_lock);
+
+ journal_close();
+
+ /* rotate the journal sets */
+ old_js = journal_old;
+ journal_old = journal_cur;
+ journal_cur = calloc(1, sizeof(journal_set));
+
+ if (journal_cur != NULL)
+ journal_new_file();
+ else
+ RRDD_LOG(LOG_CRIT, "journal_rotate: malloc(journal_set) failed\n");
+
+ pthread_mutex_unlock(&journal_lock);
+
+ journal_set_remove(old_js);
+ journal_set_free (old_js);
+
+} /* }}} static void journal_rotate */
+
+/* MUST hold journal_lock when calling */
+static void journal_done(void) /* {{{ */
+{
+ if (journal_cur == NULL)
+ return;
+
+ journal_close();
+
+ if (config_flush_at_shutdown)
+ {
+ RRDD_LOG(LOG_INFO, "removing journals");
+ journal_set_remove(journal_old);
+ journal_set_remove(journal_cur);
+ }
+ else
+ {
+ RRDD_LOG(LOG_INFO, "expedited shutdown; "
+ "journals will be used at next startup");
+ }
+
+ journal_set_free(journal_cur);
+ journal_set_free(journal_old);
+ free(journal_dir);
+
+} /* }}} 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);
+ journal_size += chars;
+
+ if (journal_size > JOURNAL_MAX)
+ journal_new_file();
+
+ 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)
+ {
+ 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))
+ {
+ size_t entry_len;
+
+ ++line;
+ if (fgets(entry, sizeof(entry), fh) == NULL)
+ break;
+ entry_len = strlen(entry);
+
+ /* check \n termination in case journal writing crashed mid-line */
+ if (entry_len == 0)
+ continue;
+ else if (entry[entry_len - 1] != '\n')
+ {
+ RRDD_LOG(LOG_NOTICE, "Malformed journal entry at line %"PRIu64, line);
+ ++fail_cnt;
+ continue;
+ }
+
+ entry[entry_len - 1] = '\0';
+
+ if (handle_request(NULL, now, entry, entry_len) == 0)
+ ++entry_cnt;
+ else
+ ++fail_cnt;
+ }
+
+ fclose(fh);