]> git.ipfire.org Git - thirdparty/knot-dns.git/commitdiff
knotd: implement update-delay
authorLibor Peltan <libor.peltan@nic.cz>
Tue, 16 Sep 2025 08:52:47 +0000 (10:52 +0200)
committerDaniel Salzman <daniel.salzman@nic.cz>
Tue, 16 Sep 2025 12:59:43 +0000 (14:59 +0200)
15 files changed:
doc/reference.rst
src/knot/catalog/generate.c
src/knot/catalog/generate.h
src/knot/conf/schema.c
src/knot/conf/schema.h
src/knot/events/handlers/ds_check.c
src/knot/events/handlers/refresh.c
src/knot/nameserver/notify.c
src/knot/nameserver/update.c
src/knot/server/server.c
src/knot/zone/zone.c
src/knot/zone/zone.h
src/knot/zone/zonedb-load.c
tests-extra/tests/zone/update_delay/test.py [new file with mode: 0644]
tests-extra/tools/dnstest/server.py

index 23f8cb7dde9b731f429f6afb363a29c94fad2de0..3f5bd4f86d858c493edd3fd0267d7bb395e80aea 100644 (file)
@@ -2721,6 +2721,7 @@ Definition of zones served by the server.
      ddns-master: remote_id
      notify: remote_id | remotes_id ...
      notify-delay: TIME
+     update-delay: TIME
      acl: acl_id ...
      master-pin-tolerance: TIME
      provide-ixfr: BOOL
@@ -2892,6 +2893,22 @@ also defines the time granularity at which NOTIFY messages are sent per zone.
 
 *Default:* ``0``
 
+.. _zone_update-delay:
+
+update-delay
+------------
+
+A time delay in seconds before a change to zone contents is made after an external
+trigger such as incoming NOTIFY or DDNS, or an internal trigger from different zone
+such as change to zone to be :ref:`reversed<zone_reverse-generate>`,
+:ref:`included from<zone_include-from>` or a member of generated catalog zone.
+
+Exception: zone changing events triggered by control socket (knotc zone-*
+commands) or by interpreted catalog are performed immediately, without configured
+delay.
+
+*Default:* ``0``
+
 .. _zone_acl:
 
 acl
index c406ffc96fc6b954b70d74a2f058bf390aa526da..f12da65f5e57a7c3fb8611e1bc85a201bf9cb847 100644 (file)
@@ -52,7 +52,7 @@ static knot_dname_t *catalog_member_owner(const knot_dname_t *member,
        return out;
 }
 
-void catalog_generate_rem(zone_t *zone, knot_zonedb_t *db_new)
+void catalog_generate_rem(conf_t *conf, zone_t *zone, knot_zonedb_t *db_new)
 {
        if (zone == NULL) {
                return;
@@ -76,12 +76,12 @@ void catalog_generate_rem(zone_t *zone, knot_zonedb_t *db_new)
        if (ret != KNOT_EOK) {
                catz->cat_members->error = ret;
        } else {
-               zone_events_schedule_now(catz, ZONE_EVENT_LOAD);
+               zone_schedule_update(conf, catz, ZONE_EVENT_LOAD);
        }
        free(owner);
 }
 
-void catalog_generate_add(zone_t *zone, knot_zonedb_t *db_new, bool property)
+void catalog_generate_add(conf_t *conf, zone_t *zone, knot_zonedb_t *db_new, bool property)
 {
        if (zone == NULL) {
                return;
@@ -110,7 +110,7 @@ void catalog_generate_add(zone_t *zone, knot_zonedb_t *db_new, bool property)
        if (ret != KNOT_EOK) {
                catz->cat_members->error = ret;
        } else {
-               zone_events_schedule_now(catz, ZONE_EVENT_LOAD);
+               zone_schedule_update(conf, catz, ZONE_EVENT_LOAD);
        }
        free(owner);
 }
index 20dfe9033483eee926a9e6df777badd59edfc27f..bd7d8387d2e8058d1ba1558c2ed9a075c78ad93f 100644 (file)
 /*!
  * \brief Add a member removal to corresponding catalog update.
  *
+ * \param conf        Configuration.
  * \param zone        Member zone.
  * \param db_new      New zone database.
  */
-void catalog_generate_rem(zone_t *zone, knot_zonedb_t *db_new);
+void catalog_generate_rem(conf_t *conf, zone_t *zone, knot_zonedb_t *db_new);
 
 /*!
  * \brief Add a member addition/prop to corresponding catalog update.
  *
+ * \param conf        Configuration.
  * \param zone        Member zone.
  * \param db_new      New zone database.
  * \param property    Property or addition indicator.
  */
-void catalog_generate_add(zone_t *zone, knot_zonedb_t *db_new, bool property);
+void catalog_generate_add(conf_t *conf, zone_t *zone, knot_zonedb_t *db_new, bool property);
 
 /*!
  * \brief Generate catalog zone contents from (full) catalog update.
index bc8b3ae7c1bdeb7c1d4f4596939529e2b7dccd28..f63fd11b8b443184b8e8b36c1d69351ee0465e4b 100644 (file)
@@ -478,7 +478,8 @@ static const yp_item_t desc_external[] = {
        { C_DDNS_MASTER,         YP_TREF,  YP_VREF = { C_RMT }, YP_FNONE, { check_ref_empty } }, \
        { C_NOTIFY,              YP_TREF,  YP_VREF = { C_RMT, C_RMTS }, YP_FMULTI | CONF_REF_EMPTY, \
                                           { check_ref } }, \
-       { C_NOTIFY_DELAY,        YP_TINT,  YP_VINT  = { -1, UINT32_MAX, 0, YP_STIME } }, \
+       { C_NOTIFY_DELAY,        YP_TINT,  YP_VINT  = { 0, UINT32_MAX, 0, YP_STIME } }, \
+       { C_UPDATE_DELAY,        YP_TINT,  YP_VINT  = { 0, UINT32_MAX, 0, YP_STIME } }, \
        { C_ACL,                 YP_TREF,  YP_VREF = { C_ACL }, YP_FMULTI, { check_ref } }, \
        { C_MASTER_PIN_TOL,      YP_TINT,  YP_VINT = { 0, UINT32_MAX, 0, YP_STIME } }, \
        { C_PROVIDE_IXFR,        YP_TBOOL, YP_VBOOL = { true } }, \
index f9b6c7b276fe9ebdb325dd996248b1bdf8575b96..3c457a683f4f66cdc384c2df87b8f4324541bb66 100644 (file)
 #define C_UDP_MAX_PAYLOAD_IPV6 "\x14""udp-max-payload-ipv6"
 #define C_UDP_WORKERS          "\x0B""udp-workers"
 #define C_UNSAFE_OPERATION     "\x10""unsafe-operation"
+#define C_UPDATE_DELAY         "\x0C""update-delay"
 #define C_UPDATE_OWNER         "\x0C""update-owner"
 #define C_UPDATE_OWNER_MATCH   "\x12""update-owner-match"
 #define C_UPDATE_OWNER_NAME    "\x11""update-owner-name"
index f00c0034e50958eedc9c6f95dc305eec437f1e74..4a7734516c7da2fc05116b69a4a116b700aed0b6 100644 (file)
@@ -23,7 +23,7 @@ int event_ds_check(conf_t *conf, zone_t *zone)
        case KNOT_NO_READY_KEY:
                break;
        case KNOT_EOK:
-               zone_events_schedule_now(zone, ZONE_EVENT_DNSSEC);
+               zone_schedule_update(conf, zone, ZONE_EVENT_DNSSEC);
                break;
        default:
                if (ctx.policy->ksk_sbm_check_interval > 0) {
index 074187da25888eaac791492e3ef96b1d4015eea5..e83a219c7c591a90b12b557b08d427a6c6f679ae 100644 (file)
@@ -1520,7 +1520,7 @@ int event_refresh(conf_t *conf, zone_t *zone)
                zone_schedule_notify(conf, zone, 1);
        }
        if (trctx.more_xfr && ret == KNOT_EOK) {
-               zone_events_schedule_now(zone, ZONE_EVENT_REFRESH);
+               zone_schedule_update(conf, zone, ZONE_EVENT_REFRESH);
        }
 
        return ret;
index 19599495f58a258a87bf73e6f6d3ad063de1d292..f16b21a1a44a0cb0ef78f1de2dff9b2d5d87c671 100644 (file)
@@ -76,7 +76,7 @@ knot_layer_state_t notify_process_query(knot_pkt_t *pkt, knotd_qdata_t *qdata)
 
        /* Incoming NOTIFY expires REFRESH timer and renews EXPIRE timer. */
        zone_set_preferred_master(zone, knotd_qdata_remote_addr(qdata));
-       zone_events_schedule_now(zone, ZONE_EVENT_REFRESH);
+       zone_schedule_update(conf(), zone, ZONE_EVENT_REFRESH);
 
        return KNOT_STATE_DONE;
 }
index 13cd31634ddb0669e311cf342de45c82189a3ab1..0e278000ef3758768e8ec7a4a7f7b76602c0cf55 100644 (file)
@@ -88,7 +88,7 @@ static int update_enqueue(zone_t *zone, knotd_qdata_t *qdata)
        pthread_mutex_unlock(&zone->ddns_lock);
 
        /* Schedule UPDATE event. */
-       zone_events_schedule_now(zone, ZONE_EVENT_UPDATE);
+       zone_schedule_update(conf(), zone, ZONE_EVENT_UPDATE);
 
        return KNOT_EOK;
 }
index 3b62af2744db7c9867cabfaa277d3aa5326264be..532f88ed2ad65ca38d0098ecbfecd9d4177ada1d 100644 (file)
@@ -33,6 +33,7 @@
 #include "knot/updates/acl.h"
 #include "knot/zone/redis.h"
 #include "knot/zone/timers.h"
+#include "knot/zone/zone.h"
 #include "knot/zone/zonedb-load.h"
 #include "knot/worker/pool.h"
 #include "contrib/base64.h"
@@ -899,7 +900,7 @@ static void rdb_process_event(redisReply *reply, knot_zonedb_t *zone_db,
        switch (type) {
        case RDB_EVENT_ZONE:
        case RDB_EVENT_UPD:
-               zone_events_schedule_now(zone, ZONE_EVENT_LOAD);
+               zone_schedule_update(conf(), zone, ZONE_EVENT_LOAD);
                break;
        default:
                break;
index 624e309bf085d10aa74cf9ad21820b2f1ee67a60..1d2169c17363419fd98b68dfac8511bbb7eae0a3 100644 (file)
@@ -486,6 +486,16 @@ void zone_schedule_notify(conf_t *conf, zone_t *zone, time_t delay)
        zone_events_schedule_at(zone, ZONE_EVENT_NOTIFY, time(NULL) + conf_delay + delay);
 }
 
+void zone_schedule_update(conf_t *conf, zone_t *zone, zone_event_type_t type)
+{
+       int64_t conf_delay = 0;
+       if (zone->contents != NULL) {
+               conf_val_t val = conf_zone_get(conf, C_UPDATE_DELAY, zone->name);
+               conf_delay = conf_int(&val);
+       }
+       zone_events_schedule_at(zone, type, time(NULL) + conf_delay);
+}
+
 zone_contents_t *zone_switch_contents(zone_t *zone, zone_contents_t *new_contents)
 {
        if (zone == NULL) {
@@ -868,7 +878,7 @@ void zone_local_notify(conf_t *conf, zone_t *zone)
        ptrnode_t *n;
        WALK_LIST(n, zone->internal_notify) {
                zone_t *to_notify = n->d;
-               zone_events_schedule_now(to_notify, zone_is_slave(conf, to_notify) ? ZONE_EVENT_REFRESH : ZONE_EVENT_LOAD);
+               zone_schedule_update(conf, to_notify, zone_is_slave(conf, to_notify) ? ZONE_EVENT_REFRESH : ZONE_EVENT_LOAD);
        }
 }
 
index a04eed16e97c732a289a7cedcfe830878b4b8138..6b6eb79a487c2b250f932f710dbbebc9e883d9eb 100644 (file)
@@ -229,6 +229,8 @@ bool zone_journal_same_serial(zone_t *zone, uint32_t serial_to);
 void zone_notifailed_clear(zone_t *zone);
 void zone_schedule_notify(conf_t *conf, zone_t *zone, time_t delay);
 
+void zone_schedule_update(conf_t *conf, zone_t *zone, zone_event_type_t type);
+
 /*!
  * \brief Atomically switch the content of the zone.
  */
index 526b40a0468fbc66507af944e543156903719ce4..1270a6b4f9240a9f828bde8eb23e61af9cba4341 100644 (file)
@@ -481,18 +481,18 @@ static knot_zonedb_t *create_zonedb_commit(conf_t *conf, server_t *server)
                                        }
                                }
                                knot_zonedb_insert(db_new, zone);
-                               catalog_generate_add(zone, db_new, false);
+                               catalog_generate_add(conf, zone, db_new, false);
                                reg_reverse(conf, db_new, zone);
                        } else if (type & CONF_IO_TUNSET) {
                                zone_t *zone = knot_zonedb_find(db_new, name);
                                unreg_reverse(zone);
                                knot_zonedb_del(db_new, name);
-                               catalog_generate_rem(zone, db_new);
+                               catalog_generate_rem(conf, zone, db_new);
                        } else {
                                zone_t *zone = knot_zonedb_find(db_new, name);
                                zone_t *old = knot_zonedb_find(db_old, name);
                                if (!same_group(old, zone)) {
-                                       catalog_generate_add(zone, db_new, true);
+                                       catalog_generate_add(conf, zone, db_new, true);
                                }
                                reg_reverse(conf, db_new, zone);
                        }
@@ -541,14 +541,14 @@ static knot_zonedb_t *create_zonedb_catalog(conf_t *conf, server_t *server,
                case CAT_UPD_ADD:
                        zone = add_member_zone(upd, db_new, server, conf);
                        knot_zonedb_insert(db_new, zone);
-                       catalog_generate_add(zone, db_new, false);
+                       catalog_generate_add(conf, zone, db_new, false);
                        reg_reverse(conf, db_new, zone);
                        break;
                case CAT_UPD_REM:
                        zone = knot_zonedb_find(db_new, upd->member);
                        unreg_reverse(zone);
                        knot_zonedb_del(db_new, upd->member);
-                       catalog_generate_rem(zone, db_new);
+                       catalog_generate_rem(conf, zone, db_new);
                        break;
                case CAT_UPD_UNIQ:
                case CAT_UPD_PROP:
@@ -561,7 +561,7 @@ static knot_zonedb_t *create_zonedb_catalog(conf_t *conf, server_t *server,
                        }
                        zone_t *old = knot_zonedb_find(db_old, upd->member);
                        if (!same_group(old, zone)) {
-                               catalog_generate_add(zone, db_new, true);
+                               catalog_generate_add(conf, zone, db_new, true);
                        }
                        reg_reverse(conf, db_new, zone);
                        break;
@@ -662,7 +662,7 @@ static knot_zonedb_t *create_zonedb_full(conf_t *conf, server_t *server,
                for (; !knot_zonedb_iter_finished(db_it); knot_zonedb_iter_next(db_it)) {
                        zone_t *zone = knot_zonedb_iter_val(db_it);
                        if (knot_zonedb_find(db_new, zone->name) == NULL) {
-                               catalog_generate_rem(zone, db_new);
+                               catalog_generate_rem(conf, zone, db_new);
                        }
                }
                knot_zonedb_iter_free(db_it);
@@ -674,9 +674,9 @@ static knot_zonedb_t *create_zonedb_full(conf_t *conf, server_t *server,
                zone_t *zone = knot_zonedb_iter_val(db_it);
                zone_t *old = knot_zonedb_find(db_old, zone->name);
                if (old == NULL) {
-                       catalog_generate_add(zone, db_new, false);
+                       catalog_generate_add(conf, zone, db_new, false);
                } else if (!same_group(old, zone)) {
-                       catalog_generate_add(zone, db_new, true);
+                       catalog_generate_add(conf, zone, db_new, true);
                }
                reg_reverse(conf, db_new, zone);
        }
diff --git a/tests-extra/tests/zone/update_delay/test.py b/tests-extra/tests/zone/update_delay/test.py
new file mode 100644 (file)
index 0000000..3f64d24
--- /dev/null
@@ -0,0 +1,64 @@
+#!/usr/bin/env python3
+
+"""
+Test of update delay.
+"""
+
+from dnstest.utils import *
+from dnstest.test import Test
+import random
+import threading
+import time
+
+t = Test()
+
+master = t.server("knot")
+slave = t.server("knot")
+zones = t.zone_rnd(2, records=10)
+
+t.link(zones, master, slave)
+
+master.update_delay = 6
+slave.update_delay = 6
+
+master.serial_policy = "unixtime"
+slave.serial_policy = "unixtime"
+
+for z in zones:
+    master.zones[z.name].zfile.update_soa(serial=int(time.time()))
+    slave.dnssec(z).enable = True # so that slave has own SOA serial management
+
+def increment_serials(server, zones, serials):
+    res = serials
+    for z in zones:
+        res[z.name] += server.update_delay
+    return res
+
+def zones_wait_eq(server, zones, serials):
+    return server.zones_wait(zones, serials, greater=True, equal=True)
+
+def send_update(up):
+    try:
+        up.try_send()
+    except:
+        pass
+
+def send_up_bg(up):
+    threading.Thread(target=send_update, args=[up]).start()
+
+t.start()
+serials = master.zones_wait(zones)
+serials = zones_wait_eq(slave, zones, serials) # initial AXFR: without delay
+
+for z in zones:
+    up = master.update(z)
+    up.add("test-update-delay-add", 3600, "A", "1.2.3.4")
+    send_up_bg(up)
+
+increment_serials(master, zones, serials)
+serials = zones_wait_eq(master, zones, serials) # DDNS processing with delay
+
+increment_serials(slave, zones, serials)
+serials = zones_wait_eq(slave, zones, serials) # slave XFRs the zone swith another delay
+
+t.end()
index 137d04defeab4d6552017636f6237e3bf69fa4fb..88b890145f3525730532057f14eabd2ad270d449 100644 (file)
@@ -204,6 +204,7 @@ class Server(object):
         self.semantic_check = True
         self.zonefile_sync = "1d"
         self.notify_delay = None
+        self.update_delay = None
         self.zonefile_load = None
         self.zonefile_skip = None
         self.zonemd_verify = None
@@ -1902,6 +1903,7 @@ class Knot(Server):
         if self.notify_delay is None:
             self.notify_delay = random.randint(0, 1)
         s.item_str("notify-delay", self.notify_delay)
+        self._str(s, "update-delay", self.update_delay)
         if self.zonemd_verify:
             s.item_str("zonemd-verify", "on")
         if self.zonemd_generate is not None: