Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ public DataSystemBuilder persistentStore(ComponentConfigurer<DataStore> persiste
*
* @param overrideSource the override source configuration, or null for none
* @return a reference to the builder
* @see FileOverrides
* @since 7.19.0
*/
public DataSystemBuilder overrides(ComponentConfigurer<OverrideSource> overrideSource) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
package com.launchdarkly.sdk.server.integrations;

import com.launchdarkly.logging.LDLogger;
import com.launchdarkly.sdk.server.integrations.FileOverrides.ChangeDetection;
import com.launchdarkly.sdk.server.subsystems.ClientContext;
import com.launchdarkly.sdk.server.subsystems.ComponentConfigurer;
import com.launchdarkly.sdk.server.subsystems.OverrideSource;

import java.nio.file.FileSystems;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Duration;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

/**
* Configures the file-based override source. Obtain an instance with {@link FileOverrides#source()},
* call {@link #filePaths(String...)} or {@link #filePaths(Path...)} to specify the files, adjust
* any other options, and pass the builder to
* {@link DataSystemBuilder#overrides(ComponentConfigurer)}.
* <p>
* For more details, see {@link FileOverrides}.
* <p>
* Flag overrides are currently experimental and subject to change.
*
* @since 7.19.0
*/
public final class FileOverrideSourceBuilder implements ComponentConfigurer<OverrideSource> {
private final List<Path> filePaths = new ArrayList<>();
private FileData.DuplicateKeysHandling duplicateKeysHandling = FileData.DuplicateKeysHandling.FAIL;
private ChangeDetection changeDetection = ChangeDetection.POLLING;
private Duration pollInterval = FileOverrides.DEFAULT_POLL_INTERVAL;

FileOverrideSourceBuilder() {}

/**
* Adds any number of files to load overrides from, specifying each file path as a string. The
* order is significant: it determines which file wins under the duplicate keys handling when the
* same key appears in more than one file.
* <p>
* A file whose name ends in {@code .yaml} or {@code .yml}, in any case, is parsed as YAML, and any
* other file is parsed as JSON. A file is read as UTF-8, or in the encoding that a byte order mark
* names.
* <p>
* Flag overrides are currently experimental and subject to change.
*
* @param filePaths path(s) to the file(s); may be absolute or relative to the current working directory
* @return the same builder
* @throws InvalidPathException if one of the parameters is not a valid file path
*/
public FileOverrideSourceBuilder filePaths(String... filePaths) throws InvalidPathException {
for (String p : filePaths) {
this.filePaths.add(Paths.get(p));
}
return this;
}

/**
* Adds any number of files to load overrides from, specifying each file path as a Path. The
* order is significant: it determines which file wins under the duplicate keys handling when the
* same key appears in more than one file.
* <p>
* A file whose name ends in {@code .yaml} or {@code .yml}, in any case, is parsed as YAML, and any
* other file is parsed as JSON. A file is read as UTF-8, or in the encoding that a byte order mark
* names.
* <p>
* Flag overrides are currently experimental and subject to change.
*
* @param filePaths path(s) to the file(s); may be absolute or relative to the current working directory
* @return the same builder
*/
public FileOverrideSourceBuilder filePaths(Path... filePaths) {
for (Path p : filePaths) {
this.filePaths.add(p);
}
return this;
}

/**
* Specifies how to handle the same key appearing in more than one file.
* <p>
* With {@link FileData.DuplicateKeysHandling#FAIL}, the default, a reload fails if the same flag or
* segment key appears in more than one file, and the previously loaded overrides stay in effect.
* With {@link FileData.DuplicateKeysHandling#IGNORE}, the entry from the first configured file that
* defines the key is kept and the others are discarded. A null value selects the default.
* <p>
* Flag overrides are currently experimental and subject to change.
*
* @param duplicateKeysHandling specifies how to handle duplicate keys
* @return the same builder
*/
public FileOverrideSourceBuilder duplicateKeysHandling(FileData.DuplicateKeysHandling duplicateKeysHandling) {
this.duplicateKeysHandling = duplicateKeysHandling == null
? FileData.DuplicateKeysHandling.FAIL : duplicateKeysHandling;
return this;
}

/**
* Selects how the source detects file changes. The default is {@link ChangeDetection#POLLING}.
* The two modes are alternatives, so setting one replaces the other. {@link ChangeDetection#WATCHING}
* requires every file path to be on the default file system; a path of another file system provider,
* such as a zip file system, can be polled but not watched, and {@link #build(ClientContext)} rejects it.
* <p>
* Flag overrides are currently experimental and subject to change.
*
* @param changeDetection the change detection mode
* @return the same builder
*/
public FileOverrideSourceBuilder changeDetection(ChangeDetection changeDetection) {
this.changeDetection = changeDetection;
return this;
}

/**
* Sets the interval between examinations of the files in {@link ChangeDetection#POLLING} mode.
* {@link ChangeDetection#WATCHING} mode ignores it. The default is
* {@link FileOverrides#DEFAULT_POLL_INTERVAL}. An interval below
* {@link FileOverrides#MINIMUM_POLL_INTERVAL} is raised to the minimum.
* <p>
* Flag overrides are currently experimental and subject to change.
*
* @param pollInterval the polling interval
* @return the same builder
*/
public FileOverrideSourceBuilder pollInterval(Duration pollInterval) {
this.pollInterval = pollInterval;
return this;
}

/**
* Called internally by the SDK to create the override source.
* <p>
* Flag overrides are currently experimental and subject to change.
*
* @param context the client context
* @return the override source
* @throws IllegalArgumentException if no file paths were specified, or the change detection mode
* or the poll interval is null
*/
@Override
public OverrideSource build(ClientContext context) {
if (filePaths.isEmpty()) {
throw new IllegalArgumentException("no file paths were specified for the file-based override source");
}
if (changeDetection == null) {
throw new IllegalArgumentException("a change detection mode is required for the file-based override source");
}
if (pollInterval == null) {
throw new IllegalArgumentException("a poll interval is required for the file-based override source");
}
// Change notifications come from the default file system's watch service, which cannot watch a
// path of another file system provider, such as a zip file system. That is a configuration
// error, reported here rather than when the source starts. Polling reads such a path through its
// own provider.
if (changeDetection == ChangeDetection.WATCHING) {
for (Path p : filePaths) {
if (!p.getFileSystem().equals(FileSystems.getDefault())) {
throw new IllegalArgumentException(
"the file-based override source can watch only files on the default file system; use polling for " + p);
}
}
}
LDLogger logger = context.getBaseLogger().subLogger("FileOverrideSource");

// A path listed more than once would make every entry of that file a duplicate of itself, so
// the repeats are dropped and the first occurrence keeps its place in the order.
Set<Path> absolutePaths = new LinkedHashSet<>();
for (Path p : filePaths) {
absolutePaths.add(p.toAbsolutePath().normalize());
}
if (absolutePaths.size() != filePaths.size()) {
logger.warn("File paths list the same file more than once; the repeats are ignored");
}

Duration effectivePollInterval = pollInterval;
if (changeDetection == ChangeDetection.POLLING && pollInterval.compareTo(FileOverrides.MINIMUM_POLL_INTERVAL) < 0) {
logger.warn("Poll interval {} is below the minimum; using {}", pollInterval, FileOverrides.MINIMUM_POLL_INTERVAL);
effectivePollInterval = FileOverrides.MINIMUM_POLL_INTERVAL;
}

return new FileOverrideSourceImpl(new ArrayList<>(absolutePaths), duplicateKeysHandling, changeDetection, effectivePollInterval, logger);
}
}
Loading
Loading