]> git.ipfire.org Git - oddments/collecty.git/commitdiff
source: Check if RRD files need migration
authorMichael Tremer <michael.tremer@ipfire.org>
Tue, 21 Oct 2025 17:36:40 +0000 (17:36 +0000)
committerMichael Tremer <michael.tremer@ipfire.org>
Tue, 21 Oct 2025 17:36:40 +0000 (17:36 +0000)
Signed-off-by: Michael Tremer <michael.tremer@ipfire.org>
src/daemon/source.c

index 13f629ac1df9bbbad1080964c74d3ac5a0c1d00f..21102f772459a6d69243e787826db79d529da337 100644 (file)
@@ -20,6 +20,7 @@
 
 #include <errno.h>
 #include <limits.h>
+#include <math.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
@@ -572,6 +573,300 @@ ERROR:
        return r;
 }
 
+static int td_source_check_step(td_source* self, const char* path, rrd_info_t* info) {
+       int step = 0;
+
+       // Find the step
+       while (info) {
+               if (td_string_equals(info->key, "step")) {
+                       switch (info->type) {
+                               case RD_I_CNT:
+                                       step = info->value.u_cnt;
+                                       break;
+
+                               default:
+                                       DEBUG(self->ctx, "%s: Step is not an integer\n", path);
+                                       return -EINVAL;
+                       }
+               }
+
+               info = info->next;
+       }
+
+       // Fail if we could not find the step
+       if (!step) {
+               ERROR(self->ctx, "Failed to determine step of %s\n", path);
+               return -EIO;
+       }
+
+       // All okay if the step matches
+       if (step == USEC_TO_SEC(HEARTBEAT / 2))
+               return 0;
+
+       // The step does not match
+       DEBUG(self->ctx, "%s: Step does not match (%d, but should be %d)\n",
+               path, step, USEC_TO_SEC(HEARTBEAT / 2));
+
+       return 1;
+}
+
+// Continues to read line as long as it matches s
+static int consume(char** line, const char* s) {
+       char* p = *line;
+
+       for (;;) {
+               // Successful if we have consumed all of s
+               if (!*s)
+                       break;
+
+               // It is bad if we have run out of input
+               if (!*p)
+                       return -EBADMSG;
+
+               // The strings don't seem to match
+               if (*p != *s)
+                       return -EBADMSG;
+
+               // Advance both pointers
+               p++;
+               s++;
+       }
+
+       // Restore the line
+       *line = p;
+
+       return 0;
+}
+
+static int td_source_get_info(td_source* self, rrd_info_t* info,
+               const char* field, const char* key, rrd_info_type_t type, rrd_info_t** result) {
+       char* p = NULL;
+       int r;
+
+       // Find the step
+       for (; info; info = info->next) {
+               // Skip anything that does not match by type
+               if (info->type != type)
+                       continue;
+
+               // Start at the beginning of the string
+               p = info->key;
+
+               // Lines must start with "ds["
+               r = consume(&p, "ds[");
+               if (r < 0)
+                       continue;
+
+               // After that, we must have the field name
+               r = consume(&p, field);
+               if (r < 0)
+                       continue;
+
+               // Then, we must have "]."
+               r = consume(&p, "].");
+               if (r < 0)
+                       continue;
+
+               // Then we must have the key
+               r = consume(&p, key);
+               if (r < 0)
+                       continue;
+
+               // Skip if we didn't reach the end of the string
+               if (*p)
+                       continue;
+
+               // Match!
+               *result = info;
+               break;
+       }
+
+       return 0;
+}
+
+static int td_source_check_field_index(td_source* self, const char* path,
+               rrd_info_t* info, const char* field, unsigned long index) {
+       rrd_info_t* i = NULL;
+       int r;
+
+       // Fetch the field
+       r = td_source_get_info(self, info, field, "index", RD_I_CNT, &i);
+       if (r < 0)
+               return r;
+
+       // The field was not found
+       if (!i) {
+               DEBUG(self->ctx, "%s: Field %s does not seem to exist\n", path, field);
+               return 1;
+       }
+
+       // All good, the index matches
+       if (i->value.u_cnt == index)
+               return 0;
+
+       // The index does not match
+       DEBUG(self->ctx, "%s: Index of field %s does not match (is %d, but should be %d)\n",
+               path, field, i->value.u_cnt, index);
+
+       return 1;
+}
+
+static int td_source_check_field_type(td_source* self, const char* path,
+               rrd_info_t* info, const char* field, const char* type) {
+       rrd_info_t* i = NULL;
+       int r;
+
+       // Fetch the field
+       r = td_source_get_info(self, info, field, "type", RD_I_STR, &i);
+       if (r < 0)
+               return r;
+
+       // The field was not found
+       if (!i) {
+               DEBUG(self->ctx, "%s: Field %s does not seem to exist\n", path, field);
+               return 1;
+       }
+
+       // All good, the type matches
+       if (td_string_equals(i->value.u_str, type))
+               return 0;
+
+       // The type does not match
+       DEBUG(self->ctx, "%s: Type of field %s does not match (is %s, but should be %s)\n",
+               path, field, i->value.u_str, type);
+
+       return 1;
+}
+
+/*
+       This is necessary as rrdtool stores the limits as double and we store integers
+*/
+static int td_source_compare_limits(double current, int target) {
+       // -1 is for unlimited which is stored as NaN
+       if (isnan(current))
+               return (target == -1);
+
+       return fabs(current - target) < 1e-9;
+}
+
+static int td_source_check_field_minimum(td_source* self, const char* path,
+               rrd_info_t* info, const char* field, int min) {
+       rrd_info_t* i = NULL;
+       int r;
+
+       // Fetch the field
+       r = td_source_get_info(self, info, field, "min", RD_I_VAL, &i);
+       if (r < 0)
+               return r;
+
+       // The field was not found
+       if (!i) {
+               DEBUG(self->ctx, "%s: Field %s does not seem to exist\n", path, field);
+               return 1;
+       }
+
+       // All good, the value matches
+       if (td_source_compare_limits(i->value.u_val, min))
+               return 0;
+
+       // The minimum does not match
+       DEBUG(self->ctx, "%s: Minimum value of field %s does not match (is %lf, but should be %d)\n",
+               path, field, i->value.u_val, min);
+
+       return 1;
+}
+
+static int td_source_check_field_maximum(td_source* self, const char* path,
+               rrd_info_t* info, const char* field, int max) {
+       rrd_info_t* i = NULL;
+       int r;
+
+       // Fetch the field
+       r = td_source_get_info(self, info, field, "max", RD_I_VAL, &i);
+       if (r < 0)
+               return r;
+
+       // The field was not found
+       if (!i) {
+               DEBUG(self->ctx, "%s: Field %s does not seem to exist\n", path, field);
+               return 1;
+       }
+
+       // All good, the value matches
+       if (td_source_compare_limits(i->value.u_val, max))
+               return 0;
+
+       // The maximum does not match
+       DEBUG(self->ctx, "%s: Maximum value of field %s does not match (is %lf, but should be %d)\n",
+               path, field, i->value.u_val, max);
+
+       return 1;
+}
+
+static int td_source_needs_migration(td_source* self, const char* path, rrd_info_t* info) {
+       unsigned long index = 0;
+       int r;
+
+       // Check if the step matches
+       r = td_source_check_step(self, path, info);
+       if (r)
+               return r;
+
+       // Check all data sources
+       for (const td_rrd_ds* ds = self->impl->rrd_dss; ds->field; ds++, index++) {
+               // Check if the field index matches
+               r = td_source_check_field_index(self, path, info, ds->field, index);
+               if (r)
+                       return r;
+
+               // Check if the field type matches
+               r = td_source_check_field_type(self, path, info, ds->field, ds->type);
+               if (r)
+                       return r;
+
+               // Check if the field minimum matches
+               r = td_source_check_field_minimum(self, path, info, ds->field, ds->min);
+               if (r)
+                       return r;
+
+               // Check if the field maximum matches
+               r = td_source_check_field_maximum(self, path, info, ds->field, ds->max);
+               if (r)
+                       return r;
+       }
+
+       // XXX Check RRAs
+
+       return 0;
+}
+
+static int td_source_migrate(td_source* self, const char* path) {
+       rrd_info_t* info = NULL;
+       int r;
+
+       // Fetch information from the RRD file
+       info = rrd_info_r(path);
+
+       // Does this RRD need migration?
+       r = td_source_needs_migration(self, path, info);
+       if (r < 0) {
+               ERROR(self->ctx, "Failed to check whether %s needs migration: %s\n", path, strerror(-r));
+               goto ERROR;
+
+       // Migrate
+       } else if (r) {
+               DEBUG(self->ctx, "%s needs migration!\n", path);
+
+               // XXX TODO
+       }
+
+ERROR:
+       if (info)
+               rrd_info_free(info);
+
+       return r;
+}
+
 /*
        Called to write all collected samples to disk
 */
@@ -601,6 +896,11 @@ int td_source_commit(td_source* self,
                }
        }
 
+       // Migrate the RRD file
+       r = td_source_migrate(self, path);
+       if (r < 0)
+               return r;
+
        // Write the samples
        r = rrd_update_r(path, NULL, num_samples, samples);
        if (r < 0) {