Skip to content

Commit 2e6db2e

Browse files
author
Benjamin Geer
authored
feat: Add feature toggles (DSP-910) (#1742)
1 parent 060b627 commit 2e6db2e

59 files changed

Lines changed: 2793 additions & 832 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎docs/03-apis/feature-toggles.md‎

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
<!---
2+
Copyright © 2015-2019 the contributors (see Contributors.md).
3+
4+
This file is part of Knora.
5+
6+
Knora is free software: you can redistribute it and/or modify
7+
it under the terms of the GNU Affero General Public License as published
8+
by the Free Software Foundation, either version 3 of the License, or
9+
(at your option) any later version.
10+
11+
Knora is distributed in the hope that it will be useful,
12+
but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
GNU Affero General Public License for more details.
15+
16+
You should have received a copy of the GNU Affero General Public
17+
License along with Knora. If not, see <http://www.gnu.org/licenses/>.
18+
-->
19+
20+
# Feature Toggles
21+
22+
Some Knora features can be turned on or off on a per-request basis.
23+
This mechanism is based on
24+
[Feature Toggles (aka Feature Flags)](https://martinfowler.com/articles/feature-toggles.html).
25+
26+
For example, a new feature that introduces a breaking API change may first be
27+
introduced with a feature toggle that leaves it disabled by default, so that clients
28+
can continue using the old functionality.
29+
30+
When the new feature is ready to be tested with client code, the Knora release notes
31+
and documentation will indicate that it can be enabled on a per-request basis, as explained
32+
below.
33+
34+
At a later date, the feature may be enabled by default, and the release notes
35+
will indicate that it can still be disabled on a per-request basis by clients
36+
that are not yet ready to use it.
37+
38+
There may be more than one version of a feature toggle. Every feature
39+
toggle has at least one version number, which is an integer. The first
40+
version is 1.
41+
42+
Most feature toggles have an expiration date, after which they will be removed.
43+
44+
## Request Header
45+
46+
A client can override one or more feature toggles by submitting the HTTP header
47+
`X-Knora-Feature-Toggles`. Its value is a comma-separated list of
48+
toggles. Each toggle consists of:
49+
50+
1. its name
51+
2. a colon
52+
3. the version number
53+
4. an equals sign
54+
5. a boolean value, which can be `on`/`off`, `yes`/`no`, or `true`/`false`
55+
56+
Using `on`/`off` is recommended for clarity. For example:
57+
58+
```
59+
X-Knora-Feature-Toggles: new-foo:2=on,new-bar=off,fast-baz:1=on
60+
```
61+
62+
A version number must be given when enabling a toggle.
63+
Only one version of each toggle can be enabled at a time.
64+
If a toggle is enabled by default, and you want a version
65+
other than the default version, simply enable the toggle,
66+
specifying the desired version number. The version number
67+
you specify overrides the default.
68+
69+
Disabling a toggle means disabling all its versions. When
70+
a toggle is disabled, you will get the functionality that you would have
71+
got before the toggle existed. Therefore, a version number cannot
72+
be given when disabling a toggle.
73+
74+
## Response Header
75+
76+
Knora API v2 and admin API responses contain the header
77+
`X-Knora-Feature-Toggles`. It lists all configured toggles,
78+
in the same format as the corresponding request header.

‎docs/03-apis/index.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,5 @@ The Knora APIs include:
2929
administering projects that use Knora as well as Knora itself.
3030
* The Knora [Util API](api-util/index.md), which is intended to be used for information retrieval
3131
about the Knora-stack itself.
32+
33+
Knora API v2 and the admin API support [Feature Toggles](feature-toggles.md).

‎docs/05-internals/design/api-v2/how-to-add-a-route.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ See the routes in that package for examples. Typically, each route
6464
route will construct a responder request message and pass it to
6565
`RouteUtilV2.runRdfRouteWithFuture` to handle the request.
6666

67-
Finally, add your `knoraApiPath` function to the `apiRoutes` member
67+
Finally, add your route's `knoraApiPath` function to the `apiRoutes` member
6868
variable in `KnoraService`. Any exception thrown inside the route will
6969
be handled by the `KnoraExceptionHandler`, so that the correct client
7070
response (including the HTTP status code) will be returned.
Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
<!---
2+
Copyright © 2015-2019 the contributors (see Contributors.md).
3+
4+
This file is part of Knora.
5+
6+
Knora is free software: you can redistribute it and/or modify
7+
it under the terms of the GNU Affero General Public License as published
8+
by the Free Software Foundation, either version 3 of the License, or
9+
(at your option) any later version.
10+
11+
Knora is distributed in the hope that it will be useful,
12+
but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
GNU Affero General Public License for more details.
15+
16+
You should have received a copy of the GNU Affero General Public
17+
License along with Knora. If not, see <http://www.gnu.org/licenses/>.
18+
-->
19+
20+
# Feature Toggles
21+
22+
For an overview of feature toggles, see
23+
[Feature Toggles (aka Feature Flags)](https://martinfowler.com/articles/feature-toggles.html).
24+
The design presented here is partly inspired by that article.
25+
26+
## Requirements
27+
28+
- It should be possible to turn features on and off by:
29+
30+
- changing a setting in `application.conf`
31+
32+
- sending a particular HTTP header value with an API request
33+
34+
- (in the future) using a web-based user interface to configure a
35+
feature toggle service that multiple subsystems can access
36+
37+
38+
- Feature implementations should be produced by factory classes,
39+
so that the code using a feature does not need to know
40+
about the toggling decision.
41+
42+
- Feature factories should use toggle configuration taken
43+
from different sources, without knowing where the configuration
44+
came from.
45+
46+
- An HTTP response should indicate which features are turned
47+
on.
48+
49+
- A feature toggle should have metadata such as a description,
50+
an expiration date, developer contact information, etc.
51+
52+
- A feature toggle should have a version number, so
53+
you can get different versions of the same feature.
54+
55+
- It should be possible to configure a toggle in `application.conf`
56+
so that its setting cannot be overridden per request.
57+
58+
- The design of feature toggles should avoid ambiguity and
59+
try to prevent situations where clients might be surprised by
60+
unexpected functionality. It should be clear what will change
61+
when a client requests a particular toggle setting. Therefore,
62+
per-request settings should require the client to be explicit
63+
about what is being requested.
64+
65+
## Design
66+
67+
### Configuration
68+
69+
### Base Configuration
70+
71+
The base configuration of feature toggles is in `application.conf`
72+
under `app.feature-toggles`. Example:
73+
74+
```
75+
app {
76+
feature-toggles {
77+
new-foo {
78+
description = "Replace the old foo routes with new ones."
79+
80+
available-versions = [ 1, 2 ]
81+
default-version = 1
82+
enabled-by-default = yes
83+
override-allowed = yes
84+
85+
expiration-date = "2021-12-01T00:00:00Z"
86+
87+
developer-emails = [
88+
"A developer <a.developer@example.org>"
89+
]
90+
}
91+
92+
new-bar {
93+
description = "Replace the old bar routes with new ones."
94+
95+
available-versions = [ 1, 2, 3 ]
96+
default-version = 3
97+
enabled-by-default = yes
98+
override-allowed = yes
99+
100+
expiration-date = "2021-12-01T00:00:00Z"
101+
102+
developer-emails = [
103+
"A developer <a.developer@example.org>"
104+
]
105+
}
106+
107+
fast-baz {
108+
description = "Replace the slower, more accurate baz route with a faster, less accurate one."
109+
110+
available-versions = [ 1 ]
111+
default-version = 1
112+
enabled-by-default = no
113+
override-allowed = yes
114+
115+
developer-emails = [
116+
"A developer <a.developer@example.org>"
117+
]
118+
}
119+
}
120+
}
121+
```
122+
123+
All fields are required except `expiration-date`.
124+
125+
Since it may not be possible to predict which toggles will need versions,
126+
all toggles must have at least one version. (If a toggle could be created
127+
without versions, and then get versions later, it would not be obvious
128+
what should happen if a client then requested the toggle without specifying
129+
a version number.) Version numbers must be an ascending sequence of
130+
consecutive integers starting from 1.
131+
132+
If `expiration-date` is provided, it must be an [`xsd:dateTimeStamp`](http://www.datypic.com/sc/xsd11/t-xsd_dateTimeStamp.html). All feature toggles
133+
should have expiration dates except for long-lived ops toggles like `fast-baz` above.
134+
135+
`KnoraSettingsFeatureFactoryConfig` reads this base configuration on startup. If
136+
a feature toggle has an expiration date in the past, a warning is logged
137+
on startup.
138+
139+
### Per-Request Configuration
140+
141+
A client can override the base configuration by submitting the HTTP header
142+
`X-Knora-Feature-Toggles`. Its value is a comma-separated list of
143+
toggles. Each toggle consists of:
144+
145+
1. its name
146+
2. a colon
147+
3. the version number
148+
4. an equals sign
149+
5. a boolean value, which can be `on`/`off`, `yes`/`no`, or `true`/`false`
150+
151+
Using `on`/`off` is recommended for clarity. For example:
152+
153+
```
154+
X-Knora-Feature-Toggles: new-foo:2=on,new-bar=off,fast-baz:1=on
155+
```
156+
157+
A version number must be given when enabling a toggle.
158+
Only one version of each toggle can be enabled at a time.
159+
If a toggle is enabled by default, and you want a version
160+
other than the default version, simply enable the toggle,
161+
specifying the desired version number. The version number
162+
you specify overrides the default.
163+
164+
Disabling a toggle means disabling all its versions. When
165+
a toggle is disabled, you will get the functionality that you would have
166+
got before the toggle existed. A version number cannot
167+
be given when disabling a toggle, because it would not
168+
be obvious what this would mean (disable all versions
169+
or only the specified version).
170+
171+
## Response Header
172+
173+
Knora API v2 and admin API responses contain the header
174+
`X-Knora-Feature-Toggles`. It lists all configured toggles,
175+
in the same format as the corresponding request header.
176+
177+
## Implementation Framework
178+
179+
A `FeatureFactoryConfig` reads feature toggles from some
180+
configuration source, and optionally delegates to a parent
181+
`FeatureFactoryConfig`.
182+
183+
`KnoraRoute` constructs a `KnoraSettingsFeatureFactoryConfig`
184+
to read the base configuration. For each request, it
185+
constructs a `RequestContextFeatureFactoryConfig`, which
186+
reads the per-request configuration and has the
187+
`KnoraSettingsFeatureFactoryConfig` as its parent.
188+
It then passes the per-request configuration object to the `makeRoute`
189+
method, which can in turn pass it to a feature factory,
190+
or send it in a request message to allow a responder to
191+
use it.
192+
193+
### Feature Factories
194+
195+
The traits `FeatureFactory` and `Feature` are just tagging traits,
196+
to make code clearer. The factory methods in a feature
197+
factory will depend on the feature, and need only be known by
198+
the code that uses the feature. The only requirement is that
199+
each factory method must take a `FeatureFactoryConfig` parameter.
200+
201+
To get a `FeatureToggle`, a feature factory
202+
calls `featureFactoryConfig.getToggle`, passing the name of the toggle.
203+
If a feature toggle has only one version, it is enough to test
204+
whether test if the toggle is enabled, by calling `isEnabled` on the toggle.
205+
206+
If the feature toggle has more than one version, call its `getMatchableState`
207+
method. To allow the compiler to check that matches on version numbers
208+
are exhaustive, this method is designed to be used with a sealed trait
209+
(extending `Version`) that is implemented by case objects representing
210+
the feature's version numbers. The method returns an instance of
211+
`MatchableState`, which is analogous to `Option`: it is either `Off`
212+
or `On`, and an instance of `On` contains one of the version objects.
213+
For example:
214+
215+
```
216+
// A trait for version numbers of the new 'foo' feature.
217+
sealed trait NewFooVersion extends Version
218+
219+
// Represents version 1 of the new 'foo' feature.
220+
case object NEW_FOO_1 extends NewFooVersion
221+
222+
// Represents version 2 of the new 'foo' feature.
223+
case object NEW_FOO_2 extends NewFooVersion
224+
225+
// The old 'foo' feature implementation.
226+
private val oldFoo = new OldFooFeature
227+
228+
// The new 'foo' feature implementation, version 1.
229+
private val newFoo1 = new NewFooVersion1Feature
230+
231+
// The new 'foo' feature implementation, version 2.
232+
private val newFoo2 = new NewFooVersion2Feature
233+
234+
def makeFoo(featureFactoryConfig: FeatureFactoryConfig): Foo = {
235+
// Get the 'new-foo' feature toggle.
236+
val fooToggle: FeatureToggle = featureFactoryConfig.getToggle("new-foo")
237+
238+
// Choose an implementation according to the toggle state.
239+
fooToggle.getMatchableState(NEW_FOO_1, NEW_FOO_2) match {
240+
case Off => oldFoo
241+
case On(NEW_FOO_1) => newFoo1
242+
case On(NEW_FOO_2) => newFoo2
243+
}
244+
}
245+
```
246+
247+
### Routes as Features
248+
249+
To select different routes according to a feature toggle:
250+
251+
- Make a feature factory that extends `KnoraRouteFactory` and `FeatureFactory`,
252+
and has a `makeRoute` method that returns different implementations,
253+
each of which extends `KnoraRoute` and `Feature`.
254+
255+
- Make a façade route that extends `KnoraRoute`, is used in
256+
`ApplicationActor.apiRoutes`, and has a `makeRoute` method that
257+
delegates to the feature factory.
258+
259+
To avoid constructing redundant route instances, each façade route needs its
260+
own feature factory class.
261+
262+
### Documenting a Feature Toggle
263+
264+
The behaviour of each possible setting of each feature toggle should be
265+
documented. Feature toggles that are configurable per request should be described
266+
in the release notes.
267+
268+
### Removing a Feature Toggle
269+
270+
To facilitate removing a feature toggle, each implementation should have:
271+
272+
- a separate file for its source code
273+
274+
- a separate file for its documentation
275+
276+
When the toggle is removed, the files that are no longer needed can be
277+
deleted.

‎docs/05-internals/design/principles/index.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,4 @@ License along with Knora. If not, see <http://www.gnu.org/licenses/>.
2626
- [Triplestore Updates](triplestore-updates.md)
2727
- [Consistency Checking](consistency-checking.md)
2828
- [Authentication](authentication.md)
29+
- [Feature Toggles](feature-toggles.md)

‎third_party/dependencies.bzl‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ ALL_WEBAPI_MAIN_DEPENDENCIES = [
153153
"//webapi/src/main/scala/org/knora/webapi/app",
154154
"//webapi/src/main/scala/org/knora/webapi/core",
155155
"//webapi/src/main/scala/org/knora/webapi/exceptions",
156+
"//webapi/src/main/scala/org/knora/webapi/feature",
156157
"//webapi/src/main/scala/org/knora/webapi/http/handler",
157158
"//webapi/src/main/scala/org/knora/webapi/http/version",
158159
"//webapi/src/main/scala/org/knora/webapi/http/version/versioninfo",

0 commit comments

Comments
 (0)