@@ -15,23 +15,21 @@ nav_order: 3
1515
1616## Initialization
1717
18- During initialization the Feature Toggles want to synchronize with the central state or lazyly create it based on the
19- fallback values if necessary. Another goal is to make sure that _ current_ validation rules are respected in all cases
20- and, for example, retroactively applied to the central state.
18+ During initialization the Feature Toggles want to synchronize with the central state. Another goal is to make sure
19+ that _ current_ validation rules are respected in all cases and retroactively applied to the central state.
2120
2221Initialization broadly has this workflow:
2322
24- - read the configuration
23+ - read and process the configuration
2524- validate fallback values and warn about invalid fallback values
2625- if Redis cannot be reached:
2726 - use fallback values as local state and stop
2827- if Redis is reachable:
2928 - read state and filter out values inconsistent with validation rules
30- - publish those validated fallback values, where corresponding keys are missing from state
3129 - use validated Redis values if possible or, if none exist, fallback values as local state
3230- subscribe to future updates from Redis
3331
34- After intialization , usage code can rely on always getting at least the fallback values (including invalid values) or,
32+ After initialization , usage code can rely on always getting at least the fallback values (including invalid values) or,
3533if possible, validated values from Redis.
3634
3735## Single Key Approach
@@ -40,26 +38,52 @@ if possible, validated values from Redis.
4038| :-------------------------------------: |
4139| _ Single Key Architecture_ |
4240
43- The current implementation uses a single Redis key to store all the state for one unique name, which is usually
44- associated with a single app, though the Feature Toggles support the case where multiple apps _ with the same
45- configuration_ use the same unique name. In the diagram you can see both examples, app 1 has a partner app, that uses
46- the same unique key and all instances of both apps, will synchronize with Redis. On the other hand app 2 is alone,
47- which is the most common use-case.
41+ The current implementation uses a single Redis key of type ` hash ` to store the state of all toggles for one unique
42+ name. A unique name is usually associated with a single app, but the library also supports the case where multiple apps
43+ _ with the same configuration_ use the same unique name.
4844
49- Using a single key on Redis for the state of all toggles has some implementation advantages:
45+ In the diagram you can see both examples, app 1 has a partner app, that uses the same unique key and all instances of
46+ both apps, will synchronize with Redis. On the other hand app 2 is alone, which is the most common use-case.
5047
51- - discovery about which toggles are maintained is trivial, no namespacing is needed
52- - pub/sub change-detection, synchronization, and lock-resolution are also easier
48+ ## Scoping
5349
54- On the other hand, there is also a disadvantage. The sync speed will degrade with the cumulative size of all toggle
55- states. In practice, if you have lots of toggles with long state strings, it will be slower than necessary.
50+ In their easiest use-cases, the Feature Toggles describe server-level state, which is _ independent_ of any runtime
51+ context. Meaning the feature toggle's value will be the same for any request, any tenant, any user, any code component,
52+ or any other abstraction layer. In practice this is often insufficient.
5653
57- ## Request-Level Toggles
54+ Scoping is our concept to allow discriminating the feature toggle values based on runtime context information.
55+ Let's take a very common example, where both ` user ` and ` tenant ` scopes are used.
5856
59- The Feature Toggles are currently implemented with server-level state. They have the limitation that their runtime
60- values _ cannot _ be different based on attributes of individual requests, for example, which tenant is making the
61- request.
57+ | ![ ] ( architecture-scopes.png ) |
58+ | :-------------------------------------------: |
59+ | _ User and tenant scopes for a feature toggle _ |
6260
63- This kind of logic can be implemented outside the Feature Toggles though. You can use a string-type toggle and encode
64- the relevant states for all tenants, or other discriminating request attributes. During the request processing, you can
65- get the toggle's state for all tenants and act based on the one making the request.
61+ To realize the distinction, runtime scope information is passed to the library as a ` Map<string, string> ` , which results
62+ in a corresponding value check order of _ descending specificity_ , e.g.:
63+
64+ - ` getFeatureValue(key) `
65+ - root scope, fallback
66+ - ` getFeatureValue(key, { tenant: cds.context.tenant }) `
67+ - ` tenant ` scope, root scope, fallback
68+ - ` getFeatureValue(key, { user: cds.context.user.id, tenant: cds.context.tenant }) `
69+ - ` user+tenant ` scope, ` user ` scope, ` tenant ` scope, root scope, fallback
70+ - ` getFeatureValue(key, { tenant: cds.context.tenant, user: cds.context.user.id }) `
71+ - ` user+tenant ` scope, ` tenant ` scope, ` user ` scope, root scope, fallback
72+
73+ The root scope is always the least specific or broadest scope and corresponds to _ not_ specifying any particular scope
74+ information. Now, the framework will go through these potential values in this order and check if any of them have been
75+ set. The first value that has been set stops the chain and is returned to the caller.
76+
77+ With this setup, we can change the resulting value for anyone with tenant ` t1 ` , _ and no other, more specific scopes_ ,
78+ by using
79+
80+ - ` changeFeatureValue(key, "new value for t1", { tenant: "t1" }) `
81+
82+ And we could change the behavior again with for the more specific tenant ` t1 ` and user ` john ` , by using
83+
84+ - ` changeFeatureValue(key, "new value just for john within t1", { user: "john", tenant: "t1" }) `
85+
86+ {: .warn}
87+ As we can see in the precedence check order, if we had just set ` changeFeatureValue(key, "new value for john", { user: "john" }) ` ,
88+ then it depends on the order used in the ` getFeatureValue ` call, whether the ` user ` scope is evaluated before
89+ the ` tenant ` scope.
0 commit comments