|
| 1 | +""" |
| 2 | +Override sources for the SDK's flag override capability. Flag overrides are currently |
| 3 | +experimental and subject to change. |
| 4 | +
|
| 5 | +Overrides are flag and segment definitions that take precedence over data received from |
| 6 | +LaunchDarkly at evaluation time, on a per-key basis. They exist for resilience during an |
| 7 | +incident. An operator can force one or more flags to a known state on a running application, |
| 8 | +whether or not the application can reach LaunchDarkly. The override stays in effect until the |
| 9 | +operator removes it. Flags not present in the override data are completely unaffected. |
| 10 | +
|
| 11 | +This module currently provides one source: :class:`FileOverrideSourceBuilder`, which reads |
| 12 | +overrides from local files and reloads them as the files change. Configure it with the data |
| 13 | +system builder: |
| 14 | +:: |
| 15 | +
|
| 16 | + from ldclient import Config, datasystem |
| 17 | + from ldclient.integrations.overrides import FileOverrideSourceBuilder |
| 18 | +
|
| 19 | + source = FileOverrideSourceBuilder(['/etc/launchdarkly/overrides.json']) |
| 20 | + config = Config(sdk_key, datasystem_config=datasystem.default().overrides(source).build()) |
| 21 | +
|
| 22 | +An evaluation that an override affects is marked. The marking is direct or transitive. It |
| 23 | +applies when the evaluated flag, a prerequisite at any depth, or a segment read during the |
| 24 | +evaluation came from the override layer. The evaluation reason's ``overrideAffected`` property |
| 25 | +reports the marking. Marked evaluations appear in analytics summary events only, under separate |
| 26 | +counters, so LaunchDarkly can distinguish them. They produce no individual evaluation events. |
| 27 | +""" |
| 28 | + |
| 29 | +from typing import List, Optional, Union |
| 30 | + |
| 31 | +from ldclient.config import DataSourceBuilderConfig, OverrideSourceBuilder |
| 32 | +from ldclient.impl.integrations.files.filedata import ( |
| 33 | + DuplicateKeysHandling, |
| 34 | + abs_file_paths, |
| 35 | + have_watchdog |
| 36 | +) |
| 37 | +from ldclient.impl.integrations.overrides.file_override_source import ( |
| 38 | + ChangeDetection, |
| 39 | + _FileOverrideSource |
| 40 | +) |
| 41 | +from ldclient.impl.util import log |
| 42 | +from ldclient.interfaces import OverrideSource |
| 43 | + |
| 44 | + |
| 45 | +class FileOverrideSourceBuilder(OverrideSourceBuilder): |
| 46 | + """ |
| 47 | + A builder for a file-based override source. Flag overrides are currently experimental and |
| 48 | + subject to change. |
| 49 | +
|
| 50 | + The source reads flag and segment overrides from one or more local files and reloads them |
| 51 | + as the files change. The files use the same document format as the file data source: |
| 52 | + each file is a JSON or YAML document with optional ``flags``, ``flagValues``, and |
| 53 | + ``segments`` members. ``flagValues`` entries are expanded into full flag definitions that |
| 54 | + return the given value for every context. YAML requires the ``pyyaml`` package. |
| 55 | +
|
| 56 | + When multiple files are configured, their entries are combined in the configured order. |
| 57 | + The order determines which file wins under the duplicate keys handling. A reload replaces |
| 58 | + the entire override set, so removing an entry from the files removes the override. A |
| 59 | + configured file that does not exist contributes no overrides: deleting a file removes its |
| 60 | + overrides, and deleting every file removes them all. A file that exists but cannot be read |
| 61 | + or parsed makes that whole reload fail. The previously loaded overrides stay in effect, the |
| 62 | + source logs the failure, retries after a short delay, and recovers on its own once the file |
| 63 | + is readable again. |
| 64 | +
|
| 65 | + Whenever the set of overrides in effect changes, including at startup, the source logs the |
| 66 | + overrides in effect and what each configured file supplied, at Info level. |
| 67 | +
|
| 68 | + By default the source polls the files for changes once per second. See |
| 69 | + :meth:`change_detection` and :meth:`poll_interval`. |
| 70 | + """ |
| 71 | + |
| 72 | + DEFAULT_POLL_INTERVAL = 1.0 |
| 73 | + """ |
| 74 | + The interval, in seconds, at which the source examines the files for changes in polling |
| 75 | + mode when no interval was specified. Because the source reads local files rather than |
| 76 | + contacting a service, a short interval keeps an override responsive during an incident at |
| 77 | + negligible cost. |
| 78 | + """ |
| 79 | + |
| 80 | + MINIMUM_POLL_INTERVAL = 1.0 |
| 81 | + """ |
| 82 | + The shortest allowed polling interval, in seconds. A configured interval below this is |
| 83 | + raised to it. The minimum exists only to prevent a pathological tight loop over the file |
| 84 | + system. |
| 85 | + """ |
| 86 | + |
| 87 | + def __init__(self, paths: Union[str, List[str]]): |
| 88 | + """ |
| 89 | + :param paths: the files to load overrides from, as a single path or a list of paths. |
| 90 | + The order is significant: it determines which file wins under the duplicate keys |
| 91 | + handling when the same key appears in more than one file. Relative paths are |
| 92 | + resolved against the current working directory when the source is built. |
| 93 | + """ |
| 94 | + self.__paths: List[str] = [paths] if isinstance(paths, str) else list(paths) |
| 95 | + self.__duplicate_keys_handling = DuplicateKeysHandling.FAIL |
| 96 | + self.__change_detection = ChangeDetection.POLLING |
| 97 | + self.__poll_interval = self.DEFAULT_POLL_INTERVAL |
| 98 | + |
| 99 | + def duplicate_keys_handling(self, handling: Union[DuplicateKeysHandling, str]) -> 'FileOverrideSourceBuilder': |
| 100 | + """ |
| 101 | + Specifies how to handle the same key appearing in more than one file. The default is |
| 102 | + :attr:`DuplicateKeysHandling.FAIL`, which treats the reload as failed and keeps the |
| 103 | + previously loaded overrides. :attr:`DuplicateKeysHandling.IGNORE` keeps the entry from |
| 104 | + the first configured file that defines the key and discards the others. |
| 105 | +
|
| 106 | + :param handling: the handling, as the enum or its string value (``"fail"`` or ``"ignore"``) |
| 107 | + """ |
| 108 | + self.__duplicate_keys_handling = DuplicateKeysHandling(handling) |
| 109 | + return self |
| 110 | + |
| 111 | + def change_detection(self, mode: Union[ChangeDetection, str]) -> 'FileOverrideSourceBuilder': |
| 112 | + """ |
| 113 | + Selects how the source detects file changes. The default is |
| 114 | + :attr:`ChangeDetection.POLLING`. The two modes are alternatives, so setting one replaces |
| 115 | + the other. :attr:`ChangeDetection.WATCHING` requires the ``watchdog`` package. |
| 116 | +
|
| 117 | + :param mode: the mode, as the enum or its string value (``"polling"`` or ``"watching"``) |
| 118 | + """ |
| 119 | + self.__change_detection = ChangeDetection(mode) |
| 120 | + return self |
| 121 | + |
| 122 | + def poll_interval(self, seconds: float) -> 'FileOverrideSourceBuilder': |
| 123 | + """ |
| 124 | + Sets the interval between examinations of the files in polling mode. Watching mode |
| 125 | + ignores it. The default is :attr:`DEFAULT_POLL_INTERVAL`. An interval below |
| 126 | + :attr:`MINIMUM_POLL_INTERVAL` is raised to the minimum. |
| 127 | +
|
| 128 | + :param seconds: the interval in seconds |
| 129 | + """ |
| 130 | + self.__poll_interval = seconds |
| 131 | + return self |
| 132 | + |
| 133 | + def build(self, config: DataSourceBuilderConfig) -> OverrideSource: # pylint: disable=unused-argument |
| 134 | + """ |
| 135 | + Builds the override source. This is called internally by the SDK. It raises |
| 136 | + ``ValueError`` when no file paths were specified or when watching mode was selected |
| 137 | + without the ``watchdog`` package. |
| 138 | + """ |
| 139 | + if len(self.__paths) == 0: |
| 140 | + raise ValueError("no file paths were specified for the file-based override source") |
| 141 | + if self.__change_detection == ChangeDetection.WATCHING and not have_watchdog: |
| 142 | + raise ValueError("the file-based override source cannot watch files for changes because the watchdog package is not installed; install it or use polling") |
| 143 | + |
| 144 | + poll_interval = self.__poll_interval |
| 145 | + if self.__change_detection == ChangeDetection.POLLING and poll_interval < self.MINIMUM_POLL_INTERVAL: |
| 146 | + log.warning("Poll interval %s is below the minimum for the file-based override source; using %s", poll_interval, self.MINIMUM_POLL_INTERVAL) |
| 147 | + poll_interval = self.MINIMUM_POLL_INTERVAL |
| 148 | + |
| 149 | + return _FileOverrideSource(abs_file_paths(self.__paths), self.__duplicate_keys_handling, self.__change_detection, poll_interval) |
| 150 | + |
| 151 | + |
| 152 | +__all__ = ['ChangeDetection', 'DuplicateKeysHandling', 'FileOverrideSourceBuilder'] |
0 commit comments