aboutsummaryrefslogtreecommitdiffstats
path: root/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/ConfigValueChangeValidatorTest.java
blob: 37a49d439bee0e35dab490c6cbcad4129d1cbd1b (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
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.model.application.validation.change;

import com.yahoo.config.model.deploy.DeployState;
import com.yahoo.test.AnotherrestartConfig;
import com.yahoo.config.ConfigInstance;
import com.yahoo.test.RestartConfig;
import com.yahoo.test.SimpletypesConfig;
import com.yahoo.config.model.api.ConfigChangeAction;
import com.yahoo.config.model.producer.AnyConfigProducer;
import com.yahoo.config.model.producer.TreeConfigProducer;
import com.yahoo.config.model.producer.AbstractConfigProducerRoot;
import com.yahoo.config.model.test.MockRoot;
import com.yahoo.vespa.model.AbstractService;
import com.yahoo.vespa.model.Host;
import com.yahoo.vespa.model.HostResource;
import com.yahoo.vespa.model.PortAllocBridge;
import com.yahoo.vespa.model.VespaModel;
import com.yahoo.vespa.model.application.validation.RestartConfigs;
import com.yahoo.vespa.model.test.utils.DeployLoggerStub;
import com.yahoo.vespa.model.test.utils.VespaModelCreatorWithMockPkg;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.util.List;
import java.util.stream.Collectors;

import static org.junit.jupiter.api.Assertions.*;

/**
 * Testing the validator on both a stub model and a real-life Vespa model.
 *
 * @author bjorncs
 */
public class ConfigValueChangeValidatorTest {

    private DeployLoggerStub logger;

    @BeforeEach
    public void resetLogger() {
        logger = new DeployLoggerStub();
    }

    /**
     * NOTE: This test method has the following assumptions about the {@link com.yahoo.vespa.model.VespaModel}:
     *    1) verbosegc and heapsize in qr-start.def is marked with restart.
     *    2) {@link com.yahoo.vespa.model.container.Container} class has QrStartConfig listed in its
     *       {@link com.yahoo.vespa.model.application.validation.RestartConfigs} attribute.
     *    3) That the config ids for the container services have a specific value.
     *
     * This test will to a certain degree ensure that the annotations in the VespaModel is correctly applied.
     */
    @Test
    void requireThatValidatorHandlesVespaModel() {
        List<ConfigChangeAction> changes = getConfigChanges(
                createVespaModel(createQrStartConfigSegment(true, 2096)),
                createVespaModel(createQrStartConfigSegment(false, 2096))
        );
        assertEquals(3, changes.size());
        assertComponentsEquals(changes, "default/container.0", 0);
        assertComponentsEquals(changes, "admin/cluster-controllers/0", 1);
        assertComponentsEquals(changes, "admin/metrics/localhost", 2);
    }

    @Test
    void requireThatDocumentTypesCanBeAddedWithoutNeedForRestart() {
        List<ConfigChangeAction> changes = getConfigChanges(
                createVespaModel("", List.of("foo")),
                createVespaModel("", List.of("foo", "bar")));
        assertEquals(0, changes.size());
    }

    @Test
    void requireThatValidatorDetectsConfigChangeFromService() {
        MockRoot oldRoot = createRootWithChildren(new SimpleConfigProducer("p", 0)
                .withChildren(new ServiceWithAnnotation("s1", 1), new ServiceWithAnnotation("s2", 2)));
        MockRoot newRoot = createRootWithChildren(new SimpleConfigProducer("p", 0)
                .withChildren(new ServiceWithAnnotation("s1", 3), new ServiceWithAnnotation("s2", 4)));
        List<ConfigChangeAction> changes = getConfigChanges(oldRoot, newRoot);
        assertEquals(2, changes.size());
        assertComponentsEquals(changes, "p/s1", 0);
        assertComponentsEquals(changes, "p/s2", 1);
        assertEquals("anotherrestart.anothervalue has changed from 1 to 3", changes.get(0).getMessage());
        assertEquals("anotherrestart.anothervalue has changed from 2 to 4", changes.get(1).getMessage());
        assertEmptyLog();
    }

    @Test
    void requireThatValidatorDetectsConfigChangeFromParentProducer() {
        MockRoot oldRoot = createRootWithChildren(new SimpleConfigProducer("p", 1)
                .withChildren(new ServiceWithAnnotation("s1", 0), new ServiceWithAnnotation("s2", 0)));
        MockRoot newRoot = createRootWithChildren(new SimpleConfigProducer("p", 2)
                .withChildren(new ServiceWithAnnotation("s1", 0), new ServiceWithAnnotation("s2", 0)));
        List<ConfigChangeAction> changes = getConfigChanges(oldRoot, newRoot);
        assertEquals(2, changes.size());
        assertComponentsEquals(changes, "p/s1", 0);
        assertComponentsEquals(changes, "p/s2", 1);
        assertEmptyLog();
    }

    @Test
    void requireThatValidatorHandlesModelsWithDifferentTopology() {
        MockRoot oldRoot = createRootWithChildren(
                new SimpleConfigProducer("p1", 0).withChildren(new ServiceWithAnnotation("s1", 1)),
                new SimpleConfigProducer("p2", 0).withChildren(new ServiceWithAnnotation("s2", 1)));
        MockRoot newRoot = createRootWithChildren(
                new ServiceWithAnnotation("s1", 2),
                new ServiceWithAnnotation("s2", 2),
                new ServiceWithAnnotation("s3", 2)
        );

        List<ConfigChangeAction> changes = getConfigChanges(oldRoot, newRoot);
        assertTrue(changes.isEmpty());
        assertEmptyLog();
    }

    @Test
    void requireThatAnnotationDoesNotHaveEmtpyConfigList() {
        assertThrows(IllegalStateException.class, () -> {
            MockRoot root = createRootWithChildren(new EmptyConfigListAnnotationService(""));
            getConfigChanges(root, root);
        });
    }

    @Test
    void requireThatConfigHasRestartMethods() {
        assertThrows(IllegalStateException.class, () -> {
            MockRoot root = createRootWithChildren(new ConfigWithMissingMethodsAnnotatedService(""));
            getConfigChanges(root, root);
        });
    }

    @Test
    void requireThatServicesAnnotatedWithNonRestartConfigProduceWarningInLog() {
        MockRoot root = createRootWithChildren(new NonRestartConfigAnnotatedService(""));
        getConfigChanges(root, root);
        assertEquals(1, logger.entries.size());
    }

    @Test
    void requireThatConfigsFromAnnotatedSuperClassesAreDetected() {
        MockRoot oldRoot = createRootWithChildren(new SimpleConfigProducer("p", 1).withChildren(
                new ChildServiceWithAnnotation("child1", 0),
                new ChildServiceWithoutAnnotation("child2", 0)));
        MockRoot newRoot = createRootWithChildren(new SimpleConfigProducer("p", 2).withChildren(
                new ChildServiceWithAnnotation("child1", 0),
                new ChildServiceWithoutAnnotation("child2", 0)));
        List<ConfigChangeAction> changes = getConfigChanges(oldRoot, newRoot);
        assertEquals(2, changes.size());
        assertComponentsEquals(changes, "p/child1", 0);
        assertComponentsEquals(changes, "p/child2", 1);
        assertEmptyLog();
    }

    private List<ConfigChangeAction> getConfigChanges(VespaModel currentModel, VespaModel nextModel) {
        ConfigValueChangeValidator validator = new ConfigValueChangeValidator();
        return validator.validate(currentModel, nextModel, new DeployState.Builder().deployLogger(logger).build());
    }

    private List<ConfigChangeAction> getConfigChanges(AbstractConfigProducerRoot currentModel,
                                                      AbstractConfigProducerRoot nextModel) {
        ConfigValueChangeValidator validator = new ConfigValueChangeValidator();
        return validator.findConfigChangesFromModels(currentModel, nextModel, logger).toList();
    }

    private static void assertComponentsEquals(List<ConfigChangeAction> changes, String name, int index) {
        assertEquals(name, changes.get(index).getServices().get(0).getConfigId());
    }

    private void assertEmptyLog() {
        assertTrue(logger.entries.isEmpty());
    }

    private static VespaModel createVespaModel(String configSegment) {
        return createVespaModel(configSegment, List.of("music"));
    }

    private static VespaModel createVespaModel(String configSegment, List<String> docTypes) {
        // Note that the configSegment is here located on root.
        return new VespaModelCreatorWithMockPkg(
                null,
                "<services version='1.0'>\n" +
                        configSegment +
                        "    <admin version='2.0'>\n" +
                        "      <adminserver hostalias='node1'/>\n" +
                        "    </admin>\n" +
                        "    <container id='default' version='1.0'>\n" +
                        "       <search/>\n" +
                        "       <nodes>\n" +
                        "           <node hostalias='node1'/>\n" +
                        "       </nodes>\n" +
                        "   </container>\n" +
                        "   <content id='basicsearch' version='1.0'>\n" +
                        "       <redundancy>1</redundancy>\n" +
                        createDocumentsSegment(docTypes) + "\n" +
                        "       <group>\n" +
                        "           <node hostalias='node1' distribution-key='0'/>\n" +
                        "       </group>\n" +
                        "       <engine>\n" +
                        "           <proton>\n" +
                        "               <searchable-copies>1</searchable-copies>\n" +
                        "           </proton>\n" +
                        "       </engine>\n" +
                        "   </content>\n" +
                        "</services>",
                createSchemas(docTypes)
        ).create();
    }

    private static String createDocumentsSegment(List<String> docTypes) {
        return "<documents>\n" +
                docTypes.stream()
                        .map(type -> "<document type='" + type + "' mode='index'/>")
                        .collect(Collectors.joining("\n")) +
                "</documents>";
    }

    private static List<String> createSchemas(List<String> docTypes) {
        return docTypes.stream()
                .map(type -> "search " + type + " { document " + type + " { } }")
                .toList();
    }

    private static String createQrStartConfigSegment(boolean verboseGc, int heapsize) {
        return "<config name='search.config.qr-start'>\n" +
                "    <jvm>\n" +
                "        <verbosegc>" + verboseGc + "</verbosegc>\n" +
                "        <heapsize>" + heapsize + "</heapsize>\n" +
                "    </jvm>" +
                "</config>\n";
    }

    private static MockRoot createRootWithChildren(TreeConfigProducer<?>... children) {
        MockRoot root = new MockRoot();
        List.of(children).forEach(root::addChild);
        root.freezeModelTopology();
        return root;
    }

    private static class NonRestartConfig extends ConfigInstance {}

    private static abstract class ServiceStub extends AbstractService {
        public ServiceStub(String name) {
            super(name);
            setHostResource(new HostResource(new Host(null, "localhost")));
        }

        @Override
        public int getPortCount() {
            return 0;
        }

        @Override public void allocatePorts(int start, PortAllocBridge from) { }
    }

    private static class SimpleConfigProducer extends TreeConfigProducer<AnyConfigProducer>
            implements RestartConfig.Producer {
        public final int value;

        public SimpleConfigProducer(String name, int value) {
            super(name);
            this.value = value;
        }

        @Override
        public void getConfig(RestartConfig.Builder builder) {
            builder.value(value);
        }

        public SimpleConfigProducer withChildren(TreeConfigProducer<?>... producer) {
            List.of(producer).forEach(this::addChild);
            return this;
        }
    }


    @RestartConfigs({RestartConfig.class, AnotherrestartConfig.class})
    private static class ServiceWithAnnotation extends ServiceStub implements AnotherrestartConfig.Producer {
        public final int anotherValue;

        public ServiceWithAnnotation(String name, int anotherValue) {
            super(name);
            this.anotherValue = anotherValue;
        }

        @Override
        public void getConfig(AnotherrestartConfig.Builder builder) {
            builder.anothervalue(anotherValue);
        }
    }

    @RestartConfigs(AnotherrestartConfig.class)
    private static class ChildServiceWithAnnotation extends ServiceWithAnnotation {
        public ChildServiceWithAnnotation(String name, int anotherValue) {
            super(name, anotherValue);
        }
    }

    private static class ChildServiceWithoutAnnotation extends ServiceWithAnnotation {
        public ChildServiceWithoutAnnotation(String name, int anotherValue) {
            super(name, anotherValue);
        }
    }

    @RestartConfigs(SimpletypesConfig.class)
    private static class NonRestartConfigAnnotatedService extends ServiceStub {
        public NonRestartConfigAnnotatedService(String name) {
            super(name);
        }
    }

    @RestartConfigs(NonRestartConfig.class)
    private static class ConfigWithMissingMethodsAnnotatedService extends ServiceStub {
        public ConfigWithMissingMethodsAnnotatedService(String name) {
            super(name);
        }
    }

    @RestartConfigs
    private static class EmptyConfigListAnnotationService extends ServiceStub {
        public EmptyConfigListAnnotationService(String name) {
            super(name);
        }
    }

}