Skip to content

Commit 430ed91

Browse files
authored
Support yaml partially (#262)
* NO-ISSUE Support yaml partially * NO-ISSUE fix test * NO-ISSUE Fix which keys are used to update dynamic properties * NO-ISSUE Move internal classes into internal package * NO-ISSUE Fix docs * NO-ISSUE Fix version in doc
1 parent 00dab27 commit 430ed91

8 files changed

Lines changed: 648 additions & 104 deletions

File tree

centraldogma/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ dependencies {
2828
implementation "org.slf4j:slf4j-api:$slf4jVersion"
2929
api "com.linecorp.centraldogma:centraldogma-client:$centralDogmaVersion"
3030
implementation "com.fasterxml.jackson.core:jackson-databind:$jacksonVersion"
31+
implementation "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:$jacksonVersion"
3132

3233
testImplementation "org.hamcrest:hamcrest:$hamcrestVersion"
3334
testImplementation "com.linecorp.centraldogma:centraldogma-testing-junit:$centralDogmaVersion"

centraldogma/src/main/java/com/linecorp/decaton/centraldogma/CentralDogmaPropertySupplier.java

Lines changed: 46 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
package com.linecorp.decaton.centraldogma;
1818

19+
import java.io.IOException;
20+
import java.io.UncheckedIOException;
1921
import java.util.List;
2022
import java.util.Optional;
2123
import java.util.concurrent.ConcurrentHashMap;
@@ -38,8 +40,8 @@
3840
import com.linecorp.centraldogma.common.Change;
3941
import com.linecorp.centraldogma.common.ChangeConflictException;
4042
import com.linecorp.centraldogma.common.PathPattern;
41-
import com.linecorp.centraldogma.common.Query;
4243
import com.linecorp.centraldogma.common.Revision;
44+
import com.linecorp.decaton.centraldogma.internal.DecatonPropertyFileFormat;
4345
import com.linecorp.decaton.processor.runtime.DynamicProperty;
4446
import com.linecorp.decaton.processor.runtime.ProcessorProperties;
4547
import com.linecorp.decaton.processor.runtime.Property;
@@ -48,12 +50,15 @@
4850

4951
/**
5052
* A {@link PropertySupplier} implementation with Central Dogma backend.
51-
*
53+
* <p>
5254
* This implementation maps property's {@link PropertyDefinition#name()} as the absolute field name in the file
5355
* on Central Dogma.
54-
*
56+
* <p>
57+
* You can use json or yaml format for the property file.
58+
* You cannot nest keys in both formats. Keys must be top-level fields.
59+
* <p>
5560
* An example JSON format would be look like:
56-
* {@code
61+
* <pre>{@code
5762
* {
5863
* "decaton.partition.concurrency": 10,
5964
* "decaton.ignore.keys": [
@@ -62,7 +67,16 @@
6267
* ],
6368
* "decaton.processing.rate.per.partition": 50
6469
* }
65-
* }
70+
* }</pre>
71+
*
72+
* An example YAML format would be look like:
73+
* <pre>{@code
74+
* decaton.partition.concurrency: 10
75+
* decaton.ignore.keys:
76+
* - "123456"
77+
* - "79797979"
78+
* decaton.processing.rate.per.partition: 50
79+
* }</pre>
6680
*/
6781
public class CentralDogmaPropertySupplier implements PropertySupplier, AutoCloseable {
6882
private static final Logger logger = LoggerFactory.getLogger(CentralDogmaPropertySupplier.class);
@@ -73,7 +87,6 @@ public class CentralDogmaPropertySupplier implements PropertySupplier, AutoClose
7387
private static final ObjectMapper objectMapper = new ObjectMapper();
7488

7589
private final Watcher<JsonNode> rootWatcher;
76-
7790
private final ConcurrentMap<String, DynamicProperty<?>> cachedProperties = new ConcurrentHashMap<>();
7891

7992
/**
@@ -94,7 +107,9 @@ public CentralDogmaPropertySupplier(CentralDogma centralDogma, String projectNam
94107
* @param fileName the name of the file containing properties as top-level fields.
95108
*/
96109
public CentralDogmaPropertySupplier(CentralDogmaRepository centralDogmaRepository, String fileName) {
97-
rootWatcher = centralDogmaRepository.watcher(Query.ofJsonPath(fileName)).start();
110+
DecatonPropertyFileFormat configFile = DecatonPropertyFileFormat.of(fileName);
111+
this.rootWatcher = configFile.createWatcher(centralDogmaRepository, fileName);
112+
98113
try {
99114
rootWatcher.awaitInitialValue(INITIAL_VALUE_TIMEOUT_SECS, TimeUnit.SECONDS);
100115
} catch (InterruptedException e) {
@@ -103,6 +118,20 @@ public CentralDogmaPropertySupplier(CentralDogmaRepository centralDogmaRepositor
103118
} catch (TimeoutException e) {
104119
throw new RuntimeException(e);
105120
}
121+
122+
rootWatcher.watch(node -> {
123+
for(ConcurrentHashMap.Entry<String, DynamicProperty<?>> cachedProperty : cachedProperties.entrySet()) {
124+
if (node.has(cachedProperty.getKey())) {
125+
try {
126+
setValue(cachedProperty.getValue(), node.get(cachedProperty.getKey()));
127+
} catch (Exception e) {
128+
// Catching Exception instead of RuntimeException, since
129+
// Kotlin-implemented DynamicProperty would throw checked exceptions
130+
logger.warn("Failed to set value updatedfrom CentralDogma for {}", cachedProperty.getKey(), e);
131+
}
132+
}
133+
}
134+
});
106135
}
107136

108137
// visible for testing
@@ -129,25 +158,13 @@ public <T> Optional<Property<T>> getProperty(PropertyDefinition<T> definition) {
129158
// for most use cases though, this cache is only filled/read once.
130159
final DynamicProperty<?> cachedProp = cachedProperties.computeIfAbsent(definition.name(), name -> {
131160
DynamicProperty<T> prop = new DynamicProperty<>(definition);
132-
Watcher<JsonNode> child = rootWatcher.newChild(jsonNode -> jsonNode.path(definition.name()));
133-
child.watch(node -> {
134-
try {
135-
setValue(prop, node);
136-
} catch (Exception e) {
137-
// Catching Exception instead of RuntimeException, since
138-
// Kotlin-implemented DynamicProperty would throw checked exceptions
139-
logger.warn("Failed to set value updated from CentralDogma for {}", definition.name(), e);
140-
}
141-
});
142161
try {
143-
JsonNode node = child.initialValueFuture().join().value(); //doesn't fail since it's a child watcher
144-
setValue(prop, node);
162+
setValue(prop, rootWatcher.latestValue().get(definition.name()));
145163
} catch (Exception e) {
146164
// Catching Exception instead of RuntimeException, since
147165
// Kotlin-implemented DynamicProperty would throw checked exceptions
148166
logger.warn("Failed to set initial value from CentralDogma for {}", definition.name(), e);
149167
}
150-
151168
return prop;
152169
});
153170

@@ -175,8 +192,7 @@ public void close() {
175192
public static CentralDogmaPropertySupplier register(CentralDogma centralDogma, String project,
176193
String repository, String filename) {
177194
final CentralDogmaRepository centralDogmaRepository = centralDogma.forRepo(project, repository);
178-
createPropertyFile(centralDogmaRepository, filename, ProcessorProperties.defaultProperties());
179-
return new CentralDogmaPropertySupplier(centralDogmaRepository, filename);
195+
return register(centralDogmaRepository, filename);
180196
}
181197

182198
/**
@@ -215,6 +231,7 @@ public static CentralDogmaPropertySupplier register(CentralDogma centralDogma, S
215231
public static CentralDogmaPropertySupplier register(CentralDogmaRepository centralDogmaRepository,
216232
String filename,
217233
PropertySupplier supplier) {
234+
218235
List<Property<?>> properties = ProcessorProperties.defaultProperties().stream().map(defaultProperty -> {
219236
Optional<? extends Property<?>> prop = supplier.getProperty(defaultProperty.definition());
220237
if (prop.isPresent()) {
@@ -236,12 +253,18 @@ private static void createPropertyFile(CentralDogmaRepository centralDogmaReposi
236253
long remainingTime = remainingTime(PROPERTY_CREATION_TIMEOUT_MILLIS, startedTime);
237254

238255
JsonNode jsonNodeProperties = convertPropertyListToJsonNode(properties);
256+
Change<?> upsert;
257+
try {
258+
upsert = DecatonPropertyFileFormat.of(fileName).createUpsertChange(fileName, jsonNodeProperties);
259+
} catch (IOException e) {
260+
throw new UncheckedIOException(e);
261+
}
239262

240263
while (!fileExists && remainingTime > 0) {
241264
try {
242265
centralDogmaRepository
243266
.commit(String.format("[CentralDogmaPropertySupplier] Property file created: %s", fileName),
244-
Change.ofJsonUpsert(fileName, jsonNodeProperties))
267+
upsert)
245268
.push(baseRevision)
246269
.get(remainingTime, TimeUnit.MILLISECONDS);
247270
logger.info("New property file {} registered on Central Dogma", fileName);
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* Copyright 2025 LINE Corporation
3+
*
4+
* Licensed under the Apache License, Version 2.0 …
5+
*/
6+
7+
package com.linecorp.decaton.centraldogma.internal;
8+
9+
import java.io.IOException;
10+
import java.util.Locale;
11+
12+
import com.fasterxml.jackson.databind.JsonNode;
13+
import com.linecorp.centraldogma.client.CentralDogmaRepository;
14+
import com.linecorp.centraldogma.client.Watcher;
15+
import com.linecorp.centraldogma.common.Change;
16+
import com.linecorp.decaton.centraldogma.CentralDogmaPropertySupplier;
17+
18+
/**
19+
* Encapsulates Central Dogma–specific concerns for reading and writing
20+
* configuration files in various text formats (JSON, YAML, ...).
21+
* <p>
22+
* Implementations convert between raw file contents managed by Central Dogma
23+
* and {@link JsonNode} values consumed by {@link CentralDogmaPropertySupplier}.
24+
*/
25+
public interface DecatonPropertyFileFormat {
26+
/**
27+
* Create and start a Watcher that emits {@link JsonNode} for each file update.
28+
*/
29+
Watcher<JsonNode> createWatcher(CentralDogmaRepository repo, String fileName);
30+
31+
/**
32+
* Serialize the given node and wrap it as Central Dogma {@link Change} for initial file creation.
33+
*/
34+
Change<?> createUpsertChange(String fileName, JsonNode initialNode) throws IOException;
35+
36+
static DecatonPropertyFileFormat of(String fileName) {
37+
String lower = fileName.toLowerCase(Locale.ROOT);
38+
return (lower.endsWith(".yml") || lower.endsWith(".yaml"))
39+
? new YamlFormat()
40+
: new JsonFormat();
41+
}
42+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/*
2+
* Copyright 2025 LINE Corporation
3+
*/
4+
5+
package com.linecorp.decaton.centraldogma.internal;
6+
7+
import com.fasterxml.jackson.databind.JsonNode;
8+
import com.linecorp.centraldogma.client.CentralDogmaRepository;
9+
import com.linecorp.centraldogma.client.Watcher;
10+
import com.linecorp.centraldogma.common.Change;
11+
import com.linecorp.centraldogma.common.Query;
12+
13+
public class JsonFormat implements DecatonPropertyFileFormat {
14+
@Override
15+
public Watcher<JsonNode> createWatcher(CentralDogmaRepository repo, String fileName) {
16+
return repo.watcher(Query.ofJsonPath(fileName)).start();
17+
}
18+
19+
@Override
20+
public Change<?> createUpsertChange(String fileName, JsonNode initialNode) {
21+
return Change.ofJsonUpsert(fileName, initialNode);
22+
}
23+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*
2+
* Copyright 2025 LINE Corporation
3+
*
4+
* Licensed under the Apache License, Version 2.0 …
5+
*/
6+
7+
package com.linecorp.decaton.centraldogma.internal;
8+
9+
import java.io.IOException;
10+
import java.io.UncheckedIOException;
11+
12+
import com.fasterxml.jackson.databind.JsonNode;
13+
import com.fasterxml.jackson.databind.ObjectMapper;
14+
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
15+
import com.linecorp.centraldogma.client.CentralDogmaRepository;
16+
import com.linecorp.centraldogma.client.Watcher;
17+
import com.linecorp.centraldogma.common.Change;
18+
import com.linecorp.centraldogma.common.Query;
19+
20+
import static com.fasterxml.jackson.dataformat.yaml.YAMLGenerator.Feature.WRITE_DOC_START_MARKER;
21+
22+
public class YamlFormat implements DecatonPropertyFileFormat {
23+
private static final ObjectMapper YAML_MAPPER = new ObjectMapper(
24+
new YAMLFactory()
25+
.disable(WRITE_DOC_START_MARKER)
26+
);
27+
28+
@Override
29+
public Watcher<JsonNode> createWatcher(CentralDogmaRepository repo, String fileName) {
30+
return repo.watcher(Query.ofText(fileName))
31+
.map(text -> {
32+
try {
33+
return YAML_MAPPER.readTree(text);
34+
} catch (IOException e) {
35+
throw new UncheckedIOException("Failed to parse YAML from " + fileName, e);
36+
}
37+
})
38+
.start();
39+
}
40+
41+
@Override
42+
public Change<?> createUpsertChange(String fileName, JsonNode initialNode) throws IOException {
43+
String yaml = YAML_MAPPER.writeValueAsString(initialNode);
44+
return Change.ofTextUpsert(fileName, yaml);
45+
}
46+
}

0 commit comments

Comments
 (0)