]> git.ipfire.org Git - thirdparty/knot-dns.git/commitdiff
redis: write to DB from zone_update_commit()
authorLibor Peltan <libor.peltan@nic.cz>
Sun, 27 Jul 2025 14:59:48 +0000 (16:59 +0200)
committerDaniel Salzman <daniel.salzman@nic.cz>
Fri, 12 Sep 2025 14:50:41 +0000 (16:50 +0200)
Knot.files
src/knot/Makefile.inc
src/knot/updates/zone-update.c
src/knot/zone/redis.c [new file with mode: 0644]
src/knot/zone/redis.h [new file with mode: 0644]
src/libknot/errcode.h
src/libknot/error.c
tests-extra/tests/redis/basic/test.py

index ced2c8aabf8c4c220c2ede4c7d4236936ab607f0..75c13c0c129d4c57870aac3e91f168bbecf2ad71 100644 (file)
@@ -389,6 +389,8 @@ src/knot/zone/measure.c
 src/knot/zone/measure.h
 src/knot/zone/node.c
 src/knot/zone/node.h
+src/knot/zone/redis.c
+src/knot/zone/redis.h
 src/knot/zone/reverse.c
 src/knot/zone/reverse.h
 src/knot/zone/semantic-check.c
index b5ecae7123c277b1d4608e288cda917f7bf08d70..e04dc44463aacaa3d7bebc43b64e922711ec949a 100644 (file)
@@ -205,6 +205,8 @@ libknotd_la_SOURCES = \
        knot/zone/measure.c                     \
        knot/zone/node.c                        \
        knot/zone/node.h                        \
+       knot/zone/redis.c                       \
+       knot/zone/redis.h                       \
        knot/zone/reverse.c                     \
        knot/zone/reverse.h                     \
        knot/zone/semantic-check.c              \
index 1fa4b7cc936135045ff5075f19dfcbf30bdbc53e..f9e11d9b87fb2d080ed32668d0b8e7e126d04445 100644 (file)
@@ -16,6 +16,7 @@
 #include "knot/zone/adds_tree.h"
 #include "knot/zone/adjust.h"
 #include "knot/zone/digest.h"
+#include "knot/zone/redis.h"
 #include "knot/zone/serial.h"
 #include "knot/zone/zone-diff.h"
 #include "knot/zone/zonefile.h"
@@ -699,6 +700,75 @@ static int commit_journal(conf_t *conf, zone_update_t *update)
        return ret;
 }
 
+static int redis_wr_rr(const knot_rrset_t *rr, void *ctx)
+{
+       return zone_redis_write_rrset(ctx, rr);
+}
+
+static int redis_wr_node(zone_node_t *node, void *ctx)
+{
+       return zone_redis_write_node(ctx, node);
+}
+
+static int commit_redis(conf_t *conf, zone_update_t *update)
+{
+       uint8_t db_instance = 0;
+       bool db_enabled = conf_zone_rdb_enabled(conf, update->zone->name, false, &db_instance);
+       if (!db_enabled) {
+               return KNOT_EOK;
+       }
+
+       struct redisContext *db_ctx = zone_redis_connect(conf);
+       if (db_ctx == NULL) {
+               return KNOT_ECONN;
+       }
+
+       bool incremental = ((update->flags & (UPDATE_INCREMENTAL | UPDATE_HYBRID)) && update->zone->contents != NULL);
+       if (incremental) {
+               zone_redis_err_t err;
+               uint32_t redis_soa = 0;
+               int soa_ret = zone_redis_serial(db_ctx, db_instance, update->zone->name, &redis_soa, err);
+               incremental = (soa_ret == KNOT_EOK && redis_soa == zone_contents_serial(update->zone->contents));
+       }
+
+       zone_redis_txn_t txn;
+       int ret = zone_redis_txn_begin(&txn, db_ctx, db_instance, update->zone->name, incremental);
+       if (ret != KNOT_EOK) {
+               zone_redis_disconnect(db_ctx);
+               return ret;
+       }
+
+       if (incremental) {
+               txn.removals = true;
+               ret = zone_update_foreach(update, false, redis_wr_rr, &txn);
+               if (ret == KNOT_EOK) {
+                       txn.removals = false;
+                       ret = zone_update_foreach(update, true, redis_wr_rr, &txn);
+               }
+       } else {
+               ret = zone_contents_apply(update->new_cont, redis_wr_node, &txn);
+               if (ret == KNOT_EOK) {
+                       ret = zone_contents_nsec3_apply(update->new_cont, redis_wr_node, &txn);
+               }
+       }
+
+       if (ret == KNOT_EOK) {
+               ret = zone_redis_txn_commit(&txn);
+       }
+       if (ret != KNOT_EOK) {
+               if (ret == KNOT_ERDB) {
+                       log_zone_error(update->zone->name, "rdb, update aborted (%s)", txn.err);
+               }
+               (void)zone_redis_txn_abort(&txn);
+       } else {
+               log_zone_info(update->zone->name, "database updated, instance %u, serial %u",
+                             db_instance, zone_contents_serial(update->new_cont));
+       }
+
+       zone_redis_disconnect(db_ctx);
+       return ret;
+}
+
 static int commit_incremental(conf_t *conf, zone_update_t *update)
 {
        assert(update);
@@ -1103,6 +1173,13 @@ int zone_update_commit(conf_t *conf, zone_update_t *update)
                return ret;
        }
 
+       ret = commit_redis(conf, update);
+       if (ret != KNOT_EOK) {
+               log_zone_error(update->zone->name, "zone database update failed (%s)", knot_strerror(ret));
+               discard_adds_tree(update);
+               return ret;
+       }
+
        ret = commit_journal(conf, update);
        if (ret != KNOT_EOK) {
                log_zone_error(update->zone->name, "journal update failed (%s)", knot_strerror(ret));
diff --git a/src/knot/zone/redis.c b/src/knot/zone/redis.c
new file mode 100644 (file)
index 0000000..e98ddd7
--- /dev/null
@@ -0,0 +1,211 @@
+/*  Copyright (C) CZ.NIC, z.s.p.o. and contributors
+ *  SPDX-License-Identifier: GPL-2.0-or-later
+ *  For more information, see <https://www.knot-dns.cz/>
+ */
+
+#include "knot/zone/redis.h"
+
+#include <string.h>
+
+#ifdef ENABLE_REDIS
+
+#include "knot/common/hiredis.h"
+
+struct redisContext *zone_redis_connect(conf_t *conf)
+{
+       return rdb_connect(conf);
+}
+
+void zone_redis_disconnect(struct redisContext *ctx)
+{
+       return rdb_disconnect(ctx);
+}
+
+static int check_reply(struct redisContext *rdb, redisReply *reply,
+                       int expected_type, zone_redis_err_t err)
+{
+       if (reply == NULL) {
+               if (rdb->err != REDIS_OK) {
+                       strlcpy(err, rdb->errstr, sizeof(zone_redis_err_t));
+               } else {
+                       strlcpy(err, "no reply", sizeof(zone_redis_err_t));
+               }
+               return KNOT_ERDB;
+       } else if (reply->type == REDIS_REPLY_ERROR) {
+               strlcpy(err, reply->str, sizeof(zone_redis_err_t));
+               return KNOT_ERDB;
+       } else if (reply->type != expected_type) {
+               strlcpy(err, "unexpected reply", sizeof(zone_redis_err_t));
+               return KNOT_ERDB;
+       } else if (reply->type == REDIS_REPLY_ARRAY && reply->elements == 0) {
+               return KNOT_ENOENT;
+       } else if (reply->type == REDIS_REPLY_STATUS && strcmp(RDB_RETURN_OK, reply->str) != 0) {
+               return KNOT_EACCES;
+       }
+
+       return KNOT_EOK;
+}
+
+int zone_redis_txn_begin(zone_redis_txn_t *txn, struct redisContext *rdb,
+                         uint8_t instance, const knot_dname_t *zone_name,
+                         bool incremental)
+{
+       if (txn == NULL || rdb == NULL || zone_name == NULL) {
+               return KNOT_EINVAL;
+       }
+
+       txn->rdb = rdb;
+       txn->instance = instance;
+       txn->origin = zone_name;
+       txn->origin_len = knot_dname_size(zone_name);
+       txn->incremental = incremental;
+       txn->removals = false;
+       txn->err[0] = '\0';
+
+       const char *cmd = txn->incremental ? RDB_CMD_UPD_BEGIN  " %b %b" :
+                                            RDB_CMD_ZONE_BEGIN " %b %b";
+
+       redisReply *reply = redisCommand(txn->rdb, cmd, txn->origin, txn->origin_len,
+                                        &txn->instance, sizeof(txn->instance));
+       int ret = check_reply(rdb, reply, REDIS_REPLY_STRING, txn->err);
+       if (ret != KNOT_EOK) {
+               freeReplyObject(reply);
+               return ret;
+       }
+       if (reply->len != sizeof(txn->rdb_txn)) {
+               freeReplyObject(reply);
+               return KNOT_EMALF;
+       }
+
+       memcpy(&txn->rdb_txn, reply->str, sizeof(txn->rdb_txn));
+       freeReplyObject(reply);
+
+       return KNOT_EOK;
+}
+
+int zone_redis_write_rrset(zone_redis_txn_t *txn, const knot_rrset_t *rr)
+{
+       if (txn == NULL || rr == NULL || (txn->removals && !txn->incremental)) {
+               return KNOT_EINVAL;
+       }
+
+       const char *cmd = !txn->incremental ? RDB_CMD_ZONE_STORE " %b %b %b %d %d %d %b" :
+                         txn->removals ?     RDB_CMD_UPD_REMOVE " %b %b %b %d %d %d %b" :
+                                             RDB_CMD_UPD_ADD    " %b %b %b %d %d %d %b";
+
+       redisReply *reply = redisCommand(txn->rdb, cmd, txn->origin, txn->origin_len,
+                                        &txn->rdb_txn, sizeof(txn->rdb_txn),
+                                        rr->owner, knot_dname_size(rr->owner), rr->type, rr->ttl,
+                                        rr->rrs.count, rr->rrs.rdata, rr->rrs.size);
+       int ret = check_reply(txn->rdb, reply, REDIS_REPLY_STATUS, txn->err);
+       if (ret != KNOT_EOK) {
+               freeReplyObject(reply);
+               return ret;
+       }
+
+       freeReplyObject(reply);
+
+       return KNOT_EOK;
+}
+
+int zone_redis_write_node(zone_redis_txn_t *txn, const zone_node_t *node)
+{
+       if (txn == NULL || node == NULL) {
+               return KNOT_EINVAL;
+       }
+
+       int ret = KNOT_EOK;
+       for (uint16_t i = 0; i < node->rrset_count && ret == KNOT_EOK; i++) {
+               knot_rrset_t rrset = node_rrset_at(node, i);
+               ret = zone_redis_write_rrset(txn, &rrset);
+       }
+
+       return ret;
+}
+
+int zone_redis_txn_commit(zone_redis_txn_t *txn)
+{
+       if (txn == NULL) {
+               return KNOT_EINVAL;
+       }
+
+       const char *cmd = txn->incremental ? RDB_CMD_UPD_COMMIT  " %b %b" :
+                                            RDB_CMD_ZONE_COMMIT " %b %b";
+
+       redisReply *reply = redisCommand(txn->rdb, cmd, txn->origin, txn->origin_len,
+                                        &txn->rdb_txn, sizeof(txn->rdb_txn));
+       int ret = check_reply(txn->rdb, reply, REDIS_REPLY_STATUS, txn->err);
+       if (ret != KNOT_EOK) {
+               freeReplyObject(reply);
+               return ret;
+       }
+
+       memset(txn, 0, sizeof(*txn));
+       freeReplyObject(reply);
+
+       return KNOT_EOK;
+}
+
+int zone_redis_txn_abort(zone_redis_txn_t *txn)
+{
+       if (txn == NULL) {
+               return KNOT_EINVAL;
+       }
+
+       const char *cmd = txn->incremental ? RDB_CMD_UPD_ABORT  " %b %b" :
+                                            RDB_CMD_ZONE_ABORT " %b %b";
+
+       redisReply *reply = redisCommand(txn->rdb, cmd, txn->origin, txn->origin_len,
+                                        &txn->rdb_txn, sizeof(txn->rdb_txn));
+       int ret = check_reply(txn->rdb, reply, REDIS_REPLY_STATUS, txn->err);
+       if (ret != KNOT_EOK) {
+               freeReplyObject(reply);
+               return ret;
+       }
+
+       memset(txn, 0, sizeof(*txn));
+       freeReplyObject(reply);
+
+       return KNOT_EOK;
+}
+
+#else // ENABLE_REDIS
+
+struct redisContext *zone_redis_connect(conf_t *conf)
+{
+       return NULL;
+}
+
+void zone_redis_disconnect(struct redisContext *ctx)
+{
+       return;
+}
+
+int zone_redis_txn_begin(zone_redis_txn_t *txn, struct redisContext *rdb,
+                         uint8_t instance, const knot_dname_t *zone_name,
+                         bool incremental)
+{
+       return KNOT_ENOTSUP;
+}
+
+int zone_redis_write_rrset(zone_redis_txn_t *txn, const knot_rrset_t *rr)
+{
+       return KNOT_ENOTSUP;
+}
+
+int zone_redis_write_node(zone_redis_txn_t *txn, const zone_node_t *node)
+{
+       return KNOT_ENOTSUP;
+}
+
+int zone_redis_txn_commit(zone_redis_txn_t *txn)
+{
+       return KNOT_ENOTSUP;
+}
+
+int zone_redis_txn_abort(zone_redis_txn_t *txn)
+{
+       return KNOT_ENOTSUP;
+}
+
+#endif // ENABLE_REDIS
diff --git a/src/knot/zone/redis.h b/src/knot/zone/redis.h
new file mode 100644 (file)
index 0000000..203c06c
--- /dev/null
@@ -0,0 +1,82 @@
+/*  Copyright (C) CZ.NIC, z.s.p.o. and contributors
+ *  SPDX-License-Identifier: GPL-2.0-or-later
+ *  For more information, see <https://www.knot-dns.cz/>
+ */
+
+#pragma once
+
+#include "knot/conf/conf.h"
+#include "knot/zone/node.h"
+#include "redis/knot.h"
+
+#ifdef ENABLE_REDIS
+#include <hiredis/hiredis.h>
+#else // ENABLE_REDIS
+struct redisContext;
+#endif // ENABLE_REDIS
+
+typedef char zone_redis_err_t[128];
+
+typedef struct {
+       struct redisContext *rdb;
+       rdb_txn_t rdb_txn;
+       uint8_t instance;
+
+       const knot_dname_t *origin;
+       size_t origin_len;
+
+       bool incremental;
+       bool removals;
+
+       zone_redis_err_t err;
+} zone_redis_txn_t;
+
+/*!
+ * \brief Wrappers to rdb_connect and rdb_disconnect not needing #ifdef ENABLE_REDIS around.
+ */
+struct redisContext *zone_redis_connect(conf_t *conf);
+void zone_redis_disconnect(struct redisContext *ctx);
+
+/*!
+ * \brief Start a writing stransaction into Redis zone database.
+ *
+ * \param txn           Transaction context structure to be filled;
+ * \param rdb           Redis context (just pass zone_redis_connect()).
+ * \param instance      Zone instance number (from configuration).
+ * \param zone_name     Zone name.
+ * \param incremental   Store incremental update (otherwise full zone rewrite).
+ *
+ * \return KNOT_E*
+ */
+int zone_redis_txn_begin(zone_redis_txn_t *txn, struct redisContext *rdb,
+                         uint8_t instance, const knot_dname_t *zone_name,
+                         bool incremental);
+
+/*!
+ * \brief Write single RRset to zone DB.
+ *
+ * \param txn    Transaction to write into.
+ * \param rr     RRset to write.
+ *
+ * \note In case of incremental transaction, txn->removals signals if the RRset should be added to removals or additions.
+ *
+ * \return KNOT_E*
+ */
+int zone_redis_write_rrset(zone_redis_txn_t *txn, const knot_rrset_t *rr);
+
+/*!
+ * \brief Calls zone_redis_write_rrset() for all RRsets in a node.
+ */
+int zone_redis_write_node(zone_redis_txn_t *txn, const zone_node_t *node);
+
+/*!
+ * \brief Commit a zone DB transaction.
+ */
+int zone_redis_txn_commit(zone_redis_txn_t *txn);
+
+/*!
+ * \brief Abort a zone DB transaction.
+ *
+ * \note You might want to ignore the return code.
+ */
+int zone_redis_txn_abort(zone_redis_txn_t *txn);
index b05f703ac25ce543345c2087f820746c7f33dbd3..c0e7342822bb3afeec8cb72c2fff263f636584ce 100644 (file)
@@ -101,6 +101,7 @@ enum knot_error {
        KNOT_ECPUCOMPAT,
        KNOT_EMODINVAL,
        KNOT_EEXTERNAL,
+       KNOT_ERDB,
 
        KNOT_GENERAL_ERROR = -900,
 
index c82a5bbc37572a9eaa5fdd330c646570dd3f562a..37f3a1c8fe7ee729fe6b540d54c0f6e18a50d819 100644 (file)
@@ -100,6 +100,7 @@ static const struct error errors[] = {
        { KNOT_ECPUCOMPAT,   "incompatible CPU architecture" },
        { KNOT_EMODINVAL,    "invalid module" },
        { KNOT_EEXTERNAL,    "external validation failed" },
+       { KNOT_ERDB,         "zone database error" },
 
        { KNOT_GENERAL_ERROR, "unknown general error" },
 
index aff00c497b4c26d9261c1d1c26b9e083422dbc16..c140f094d5c4b86e4a947ff6e36db8ee0718275a 100644 (file)
@@ -3,6 +3,7 @@
 '''Test master-slave-like replication using Redis database.'''
 
 from dnstest.test import Test
+from dnstest.utils import *
 
 t = Test(redis=True)
 
@@ -25,22 +26,76 @@ t.start()
 
 master.zones_wait(zones)
 
-master.ctl("zone-flush", wait=True)
-#slave.ctl("zone-reload")
-
+# Test zone stored by master and loaded by slave
 serials = slave.zones_wait(zones)
 t.xfr_diff(master, slave, zones)
 
+# Test incremental change stored by master and loaded by slave
 for z in zones:
     up = master.update(z)
     up.add("suppnot1", 3600, "A", "1.2.3.4")
+    up.delete("mail", "A", "192.0.2.3")
+    up.send()
+
+serials2 = slave.zones_wait(zones, serials)
+t.xfr_diff(master, slave, zones) # AXFR diff
+t.xfr_diff(master, slave, zones, serials) # IXFR diff
+for z in zones:
+    resp = slave.dig("suppnot1." + z.name, "A")
+    resp.check(rcode="NOERROR", rdata="1.2.3.4")
+
+# Test yet another incremental change
+for z in zones:
+    up = master.update(z)
+    up.delete("suppnot1", "A", "1.2.3.4")
+    up.add("suppnot1", 1800, "A", "1.2.3.5")
     up.send()
 
-t.sleep(2)
-master.ctl("zone-flush", wait=True)
-#slave.ctl("zone-reload")
+serials3 = slave.zones_wait(zones, serials2)
+t.xfr_diff(master, slave, zones)
+t.xfr_diff(master, slave, zones, serials)
+t.xfr_diff(master, slave, zones, serials2)
+for z in zones:
+    resp = slave.dig("suppnot1." + z.name, "A")
+    resp.check(rcode="NOERROR", nordata="1.2.3.4", rdata="1.2.3.5", ttl=1800)
+
+# Test no change
+slave.ctl("zone-reload", wait=True)
+uptodate_log = slave.log_search_count("database is up-to-date")
+if uptodate_log != len(zones):
+    set_err("UP-TO-DATE LOGGED %dx" % uptodate_log)
 
-slave.zones_wait(zones, serials)
+# Add to DB manually. Slave will diverge from master.
+for z in zones:
+    txn = t.redis.cli("knot.upd.begin", z.name, master.zones[z.name].redis_out)
+    r = t.redis.cli("knot.upd.remove", z.name, txn, "example.com. 3600 in soa dns1.example.com. hostmaster.example.com. %d 10800 3600 1209600 7200" % serials3[z.name])
+    r = t.redis.cli("knot.upd.add", z.name, txn, "example.com. 3600 in soa dns1.example.com. hostmaster.example.com. %d 10800 3600 1209600 7200" % (serials3[z.name] + 1))
+    r = t.redis.cli("knot.upd.add", z.name, txn, "txtadd 3600 A 1.2.3.4")
+    r = t.redis.cli("knot.upd.commit", z.name, txn)
+
+    r = t.redis.cli("knot.upd.load", z.name, master.zones[z.name].redis_out, str(serials3[z.name]))
+    if not "txtadd" in r:
+        set_err("NO TXTADD IN UPD")
+
+serials4 = slave.zones_wait(zones, serials3)
+for z in zones:
+    resp = slave.dig("txtadd." + z.name, "A")
+    resp.check(rcode="NOERROR", rdata="1.2.3.4")
+
+# Update master with double SOA increment, it shall overwrite with greater serial and different contents.
+for z in zones:
+    up = master.update(z)
+    up.add(z.name, 3600, "SOA", "dns1.example.com. hostmaster.example.com. %d 10800 3600 1209600 7200" % (serials3[z.name] + 2))
+    up.delete("suppnot1", "A", "1.2.3.5")
+    up.add("suppnot1", 900, "A", "1.2.3.5")
+    up.send()
+
+serials5 = slave.zones_wait(zones, serials4)
+for z in zones:
+    resp = slave.dig("txtadd." + z.name, "A")
+    resp.check(rcode="NXDOMAIN", nordata="1.2.3.4")
+    resp = slave.dig("suppnot1." + z.name, "A")
+    resp.check(rcode="NOERROR", nordata="1.2.3.4", rdata="1.2.3.5", ttl=900)
 t.xfr_diff(master, slave, zones)
 
 # SOA serial logic rotation