1# Copyright (C) Internet Systems Consortium, Inc. ("ISC")
2#
3# SPDX-License-Identifier: MPL-2.0
4#
5# This Source Code Form is subject to the terms of the Mozilla Public
6# License, v. 2.0.  If a copy of the MPL was not distributed with this
7# file, you can obtain one at https://mozilla.org/MPL/2.0/.
8#
9# See the COPYRIGHT file distributed with this work for additional
10# information regarding copyright ownership.
11
12import concurrent.futures
13import os
14import subprocess
15import time
16
17
18def run_rndc(server, rndc_command):
19    """
20    Send the specified 'rndc_command' to 'server' with a timeout of 10 seconds
21    """
22    rndc = os.getenv("RNDC")
23    port = os.getenv("CONTROLPORT")
24
25    cmdline = [rndc, "-c", "../_common/rndc.conf", "-p", port, "-s", server]
26    cmdline.extend(rndc_command)
27
28    subprocess.check_output(cmdline, stderr=subprocess.STDOUT, timeout=10)
29
30
31def rndc_loop(test_state, domain):
32    """
33    Run "rndc addzone", "rndc modzone", and "rndc delzone" in a tight loop
34    until the test is considered finished, ignoring errors
35    """
36    rndc_commands = [
37        ["addzone", domain, '{ type primary; file "example.db"; };'],
38        [
39            "modzone",
40            domain,
41            '{ type primary; file "example.db"; allow-transfer { any; }; };',
42        ],
43        ["delzone", domain],
44    ]
45
46    while not test_state["finished"]:
47        for command in rndc_commands:
48            try:
49                run_rndc("10.53.0.3", command)
50            except subprocess.SubprocessError:
51                pass
52
53
54def check_if_server_is_responsive():
55    """
56    Check if server status can be successfully retrieved using "rndc status"
57    """
58    try:
59        run_rndc("10.53.0.3", ["status"])
60        return True
61    except subprocess.SubprocessError:
62        return False
63
64
65def test_rndc_deadlock():
66    """
67    Test whether running "rndc addzone", "rndc modzone", and "rndc delzone"
68    commands concurrently does not trigger a deadlock
69    """
70    test_state = {"finished": False}
71
72    # Create 4 worker threads running "rndc" commands in a loop.
73    with concurrent.futures.ThreadPoolExecutor() as executor:
74        for i in range(1, 5):
75            domain = "example%d" % i
76            executor.submit(rndc_loop, test_state, domain)
77
78        # Run "rndc status" 10 times, with 1-second pauses between attempts.
79        # Each "rndc status" invocation has a timeout of 10 seconds.  If any of
80        # them fails, the loop will be interrupted.
81        server_is_responsive = True
82        attempts = 10
83        while server_is_responsive and attempts > 0:
84            server_is_responsive = check_if_server_is_responsive()
85            attempts -= 1
86            time.sleep(1)
87
88        # Signal worker threads that the test is finished.
89        test_state["finished"] = True
90
91    # Check whether all "rndc status" commands succeeded.
92    assert server_is_responsive
93