aboutsummaryrefslogtreecommitdiffstats
path: root/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/dns/NameServiceQueueTest.java
blob: 36619d4ca93548ccd55038aaea497e4257c54084 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.controller.dns;

import com.yahoo.config.provision.HostName;
import com.yahoo.config.provision.zone.ZoneId;
import com.yahoo.vespa.hosted.controller.api.integration.dns.AliasTarget;
import com.yahoo.vespa.hosted.controller.api.integration.dns.DirectTarget;
import com.yahoo.vespa.hosted.controller.api.integration.dns.LatencyAliasTarget;
import com.yahoo.vespa.hosted.controller.api.integration.dns.MemoryNameService;
import com.yahoo.vespa.hosted.controller.api.integration.dns.NameService;
import com.yahoo.vespa.hosted.controller.api.integration.dns.Record;
import com.yahoo.vespa.hosted.controller.api.integration.dns.Record.Type;
import com.yahoo.vespa.hosted.controller.api.integration.dns.RecordData;
import com.yahoo.vespa.hosted.controller.api.integration.dns.RecordName;
import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
import com.yahoo.vespa.hosted.controller.dns.NameServiceQueue.Priority;
import org.junit.jupiter.api.Test;

import java.util.ArrayDeque;
import java.util.Deque;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.function.Consumer;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertTrue;

/**
 * @author mpolden
 */
public class NameServiceQueueTest {

    @Test
    void test_queue() {
        var nameService = new MemoryNameService();
        var r1 = new Record(Record.Type.CNAME, RecordName.from("cname.vespa.oath.cloud"), RecordData.from("example.com"));
        var r2 = new Record(Record.Type.TXT, RecordName.from("txt.example.com"), RecordData.from("text"));
        var r3 = List.of(new Record(Record.Type.ALIAS, RecordName.from("alias.example.com"),
                        new LatencyAliasTarget(HostName.of("alias1"),
                                "dns-zone-01",
                                ZoneId.from("prod", "us-north-1")).pack()),
                new Record(Record.Type.ALIAS, RecordName.from("alias.example.com"),
                        new LatencyAliasTarget(HostName.of("alias2"),
                                "dns-zone-02",
                                ZoneId.from("prod", "us-north-2")).pack()));
        var o0 = Optional.<TenantAndApplicationId>empty();
        var o1 = Optional.of(TenantAndApplicationId.from("t", "a"));
        var o2 = Optional.of(TenantAndApplicationId.from("t", "b"));
        var req1 = new CreateRecord(o0, r1);
        var req2 = new CreateRecords(o1, List.of(r2));
        var req3 = new CreateRecords(o2, r3);
        var req4 = new RemoveRecords(o0, r3.get(0).type(), r3.get(0).name());
        var req5 = new RemoveRecords(o1, r2.type(), r2.name(), r2.data());
        var req6 = new RemoveRecords(o2, Record.Type.CNAME, r1.name(), r1.data());

        // Add requests with different priorities and dispatch first one
        var queue = NameServiceQueue.EMPTY.with(req2).with(req1, Priority.high);
        assertEquals(2, queue.requests().size());
        queue = queue.dispatchTo(nameService, 1);
        assertEquals(r1, nameService.findRecords(r1.type(), r1.name()).get(0));
        assertEquals(1, queue.requests().size());
        assertEquals(req2, queue.requests().iterator().next());

        // Dispatch remaining requests
        queue = queue.dispatchTo(nameService, 10);
        assertTrue(queue.requests().isEmpty());
        assertEquals(r2, nameService.findRecords(r2.type(), r2.name()).get(0));

        // Dispatch from empty queue
        assertSame(queue, queue.dispatchTo(nameService, 10));

        // Dispatch create alias
        queue = queue.with(req3).dispatchTo(nameService, 1);
        assertEquals(r3, nameService.findRecords(Record.Type.ALIAS, r3.get(0).name()));

        // Dispatch removals
        queue = queue.with(req4).with(req5).dispatchTo(nameService, 2);
        assertTrue(nameService.findRecords(r2.type(), r2.name()).isEmpty(), "Removed " + r2);
        assertTrue(nameService.findRecords(Record.Type.ALIAS, r3.get(0).name()).isEmpty(), "Removed " + r3);

        // Dispatch removals by data
        queue = queue.with(req6).dispatchTo(nameService, 1);
        assertTrue(queue.requests().isEmpty());
        assertTrue(nameService.findRecords(Record.Type.CNAME, r1.name()).isEmpty(), "Removed " + r1);

        // Keep n last requests
        queue = queue.with(req1).with(req2).with(req3).with(req4).with(req6)
                .last(2);
        assertEquals(List.of(req4, req6), List.copyOf(queue.requests()));
        assertSame(queue, queue.last(2));
        assertSame(queue, queue.last(10));
        assertTrue(queue.last(0).requests().isEmpty());

        // Keep n first requests
        queue = NameServiceQueue.EMPTY.with(req1).with(req2).with(req3).with(req4).with(req6)
                .first(3);
        assertEquals(List.of(req1, req2, req3), List.copyOf(queue.requests()));
        assertSame(queue, queue.first(3));
        assertSame(queue, queue.first(10));
        assertTrue(queue.first(0).requests().isEmpty());

        // Remove some requests
        queue = new NameServiceQueue(List.of(req1, req2, req2, req3)).without(new NameServiceQueue(List.of(req1, req2)));
        assertEquals(List.of(req2, req3), queue.requests());
    }

    @Test
    void test_failing_requests() {
        Deque<Consumer<RecordName>> expectations = new ArrayDeque<>();
        var nameService = new NameService() {
            @Override public Record createRecord(Type type, RecordName name, RecordData data) {
                var expectation = expectations.poll();
                assertNotNull(expectation, "unexpected dispatch; add more expectations, or fix the bug!");
                expectation.accept(name);
                return null;
            }
            @Override public List<Record> createAlias(RecordName name, Set<AliasTarget> targets) { throw new UnsupportedOperationException(); }
            @Override public List<Record> createDirect(RecordName name, Set<DirectTarget> targets) { throw new UnsupportedOperationException(); }
            @Override public List<Record> createTxtRecords(RecordName name, List<RecordData> txtRecords) { throw new UnsupportedOperationException(); }
            @Override public List<Record> findRecords(Type type, RecordName name) { return List.of(); }
            @Override public void updateRecord(Record record, RecordData newData) { throw new UnsupportedOperationException(); }
            @Override public void removeRecords(List<Record> record) { throw new UnsupportedOperationException(); }
        };

        var owner0 = Optional.<TenantAndApplicationId>empty();
        var owner1 = Optional.of(TenantAndApplicationId.from("t", "a"));
        var owner2 = Optional.of(TenantAndApplicationId.from("t", "b"));

        var rec1 = new Record(Type.A, RecordName.from("one"), RecordData.from("data"));
        var rec2 = new Record(Type.A, RecordName.from("two"), RecordData.from("data"));
        var rec3 = new Record(Type.A, RecordName.from("three"), RecordData.from("data"));
        var rec4 = new Record(Type.A, RecordName.from("four"), RecordData.from("data"));

        var req1 = new CreateRecord(owner0, rec1);
        var req2 = new CreateRecord(owner1, rec2);
        var req3 = new CreateRecord(owner1, rec3);
        var req4 = new CreateRecord(owner2, rec4);
        var req5 = new CreateRecord(owner1, rec4);
        var req6 = new CreateRecord(owner0, rec1);
        var req7 = new CreateRecord(owner1, rec4);
        var req8 = new CreateRecord(owner0, rec2);

        var base = List.<NameServiceRequest>of(req1, req2, req3, req4, req5, req6, req7, req8);

        // failing operator request, nothing happens repeatedly
        RuntimeException exception = new RuntimeException();
        for (int i = 0; i < 4; i++) expectations.add(name -> { assertEquals(rec1.name(), name); throw exception; });
        assertEquals(base,
                     new NameServiceQueue(base).dispatchTo(nameService, 4).requests());
        assertEquals(0, expectations.size());

        // operator request OK, owner1 fails first request, owner2 is moved first in the queue, but not yet dispatched
        expectations.add(name -> { assertEquals(rec1.name(), name); });
        expectations.add(name -> { assertEquals(rec2.name(), name); throw exception; });
        assertEquals(List.of(req4, req2, req3, req5, req6, req7, req8),
                     new NameServiceQueue(base).dispatchTo(nameService, 2).requests());
        assertEquals(0, expectations.size());

        // operator request OK, owner1 fails first request, then owner2 gets to run one request, then owner1's first request is attempted repeatedly
        expectations.add(name -> { assertEquals(rec1.name(), name); });
        expectations.add(name -> { assertEquals(rec2.name(), name); throw exception; });
        expectations.add(name -> { assertEquals(rec4.name(), name); });
        expectations.add(name -> { assertEquals(rec2.name(), name); throw exception; });
        expectations.add(name -> { assertEquals(rec2.name(), name); throw exception; });
        assertEquals(List.of(req2, req3, req5, req6, req7, req8),
                     new NameServiceQueue(base).dispatchTo(nameService, 5).requests());
        assertEquals(0, expectations.size());

        expectations.add(name -> { assertEquals(rec1.name(), name); throw exception; });    // operator fails
        expectations.add(name -> { assertEquals(rec1.name(), name); });                     // operator succeeds
        expectations.add(name -> { assertEquals(rec2.name(), name); });                     // owner1 succeeds
        expectations.add(name -> { assertEquals(rec3.name(), name); throw exception; });    // owner1 fails
        expectations.add(name -> { assertEquals(rec4.name(), name); throw exception; });    // owner2 fails
        expectations.add(name -> { assertEquals(rec3.name(), name); throw exception; });    // owner1 fails
        expectations.add(name -> { assertEquals(rec4.name(), name); });                     // owner2 succeeds
        expectations.add(name -> { assertEquals(rec3.name(), name); });                     // owner1 succeeds
        expectations.add(name -> { assertEquals(rec4.name(), name); });                     // owner1 succeeds
        expectations.add(name -> { assertEquals(rec1.name(), name); throw exception; });    // operator fails
        expectations.add(name -> { assertEquals(rec1.name(), name); });                     // operator succeeds
        expectations.add(name -> { assertEquals(rec4.name(), name); throw exception; });    // owner1 fails
        expectations.add(name -> { assertEquals(rec4.name(), name); });                     // owner1 succeeds
        expectations.add(name -> { assertEquals(rec2.name(), name); });                     // operator succeeds
        assertEquals(List.of(),
                     new NameServiceQueue(base).dispatchTo(nameService, 100).requests());
        assertEquals(0, expectations.size());

        // Finally, let the queue fill past its capacity, and see that failed requests are simply dropped instead.
        expectations.add(name -> { assertEquals(rec1.name(), name); throw exception; });
        expectations.add(name -> { assertEquals(rec2.name(), name); });
        expectations.add(name -> { assertEquals(rec1.name(), name); throw exception; });
        expectations.add(name -> { assertEquals(rec2.name(), name); });
        var full = new LinkedList<NameServiceRequest>();
        for (int i = 0; i < NameServiceQueue.QUEUE_CAPACITY; i++) {
            full.add(req1);
            full.add(req2);
        }
        assertEquals(full.subList(4, full.size()),
                     new NameServiceQueue(full).dispatchTo(nameService, 4).requests());

        // However, if the queue is even fuller, at the end of a dispatch run, the oldest requests are discarded too.
        full.add(req3);
        full.add(req4);
        assertEquals(full.subList(2, full.size()),
                     new NameServiceQueue(full).dispatchTo(nameService, 0).requests());
    }

}