New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: Add feature toggles (DSP-910) #1742
Changes from 39 commits
fcd889f
53f9d4a
60498d6
56585b9
0eac91c
e09b428
22b1d7a
2e1cde5
de2bf86
e2d95a4
d6985b1
ba4dfe9
d9f8f6e
8159d51
69d1336
a7e2dd0
e8d1464
8876dc1
e83eb06
4823756
b2fdc71
9f43b94
dfa805f
18134ab
e341ffa
830065c
5281984
8392570
f216e03
f4c5733
c2008e0
2918768
d8fedca
e40fb84
09cd8fe
e5b7404
f17d89b
4f44afe
3bb2024
bb55982
27de670
9fe4429
5474705
4c94f6e
cbe3885
6b34321
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
<!--- | ||
Copyright © 2015-2019 the contributors (see Contributors.md). | ||
|
||
This file is part of Knora. | ||
|
||
Knora is free software: you can redistribute it and/or modify | ||
it under the terms of the GNU Affero General Public License as published | ||
by the Free Software Foundation, either version 3 of the License, or | ||
(at your option) any later version. | ||
|
||
Knora is distributed in the hope that it will be useful, | ||
but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
GNU Affero General Public License for more details. | ||
|
||
You should have received a copy of the GNU Affero General Public | ||
License along with Knora. If not, see <http://www.gnu.org/licenses/>. | ||
--> | ||
|
||
# Feature Toggles | ||
|
||
Some Knora features can be turned or off on a per-request basis. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess: "Some Knora features can be turned on or off on a per-request basis."? |
||
This mechanism is based on | ||
[Feature Toggles (aka Feature Flags)](https://martinfowler.com/articles/feature-toggles.html). | ||
|
||
For example, a new feature that introduces a breaking API change may first be | ||
introduced with a feature toggle that leaves it disabled by default, so that clients | ||
can continue using the old functionality. | ||
|
||
When the new feature is ready to be tested with client code, the Knora release notes | ||
and documentation will indicate that it can be enabled on a per-request basis, as explained | ||
below. | ||
|
||
At a later date, the feature may be enabled by default, and the release notes | ||
will indicate that it can still be disabled on a per-request basis by clients | ||
that are not yet ready to use it. | ||
|
||
There may be more than one version of a feature toggle. Every feature | ||
toggle has at least one version number, which is an integer. The first | ||
version is 1. | ||
|
||
Most feature toggles have an expiration date, after which they will be removed. | ||
|
||
## Request Header | ||
|
||
A client can override one or more feature toggles by submitting the HTTP header | ||
`X-Knora-Feature-Toggles`. Its value is a comma-separated list of | ||
toggles. Each toggle consists of: | ||
|
||
1. its name | ||
2. a colon | ||
3. the version number | ||
4. an equals sign | ||
5. a boolean value, which can be `on`/`off`, `yes`/`no`, or `true`/`false` | ||
|
||
Using `on`/`off` is recommended for clarity. For example: | ||
|
||
``` | ||
X-Knora-Feature-Toggles: new-foo:2=on,fast-bar:1=on | ||
``` | ||
|
||
A version number must be given when enabling a toggle. | ||
It is an error to specify a version number when disabling | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. well, in my opinion, the user should be able to specify which version of the toggle must be disabled. |
||
a toggle. | ||
|
||
## Response Header | ||
|
||
Knora API v2 and admin API responses contain the header | ||
`X-Knora-Feature-Toggles-Enabled`, whose value is a comma-separated, | ||
unordered list of toggles that are enabled. The response to the | ||
example above would be: | ||
|
||
``` | ||
X-Knora-Feature-Toggles-Enabled: new-foo:2,fast-bar:1 | ||
``` |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,244 @@ | ||
<!--- | ||
Copyright © 2015-2019 the contributors (see Contributors.md). | ||
|
||
This file is part of Knora. | ||
|
||
Knora is free software: you can redistribute it and/or modify | ||
it under the terms of the GNU Affero General Public License as published | ||
by the Free Software Foundation, either version 3 of the License, or | ||
(at your option) any later version. | ||
|
||
Knora is distributed in the hope that it will be useful, | ||
but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
GNU Affero General Public License for more details. | ||
|
||
You should have received a copy of the GNU Affero General Public | ||
License along with Knora. If not, see <http://www.gnu.org/licenses/>. | ||
--> | ||
|
||
# Feature Toggles | ||
|
||
For an overview of feature toggles, see | ||
[Feature Toggles (aka Feature Flags)](https://martinfowler.com/articles/feature-toggles.html). | ||
The design presented here is partly inspired by that article. | ||
|
||
## Requirements | ||
|
||
- It should be possible to turn features on and off by: | ||
|
||
- changing a setting in `application.conf` | ||
|
||
- sending a particular HTTP header value with an API request | ||
|
||
- (in the future) using a web-based user interface to configure a | ||
feature toggle service that multiple subsystems can access | ||
|
||
|
||
- Feature implementations should be produced by factory classes, | ||
so that the code using a feature does not need to know | ||
about the toggling decision. | ||
|
||
- Feature factories should use toggle configuration taken | ||
from different sources, without knowing where the configuration | ||
came from. | ||
|
||
- An HTTP response should indicate which features are turned | ||
on. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is the response header also going to return turned off toggles? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Currently it only returns enabled toggles. I was thinking that if a toggle is turned off by default because the feature isn't ready to use yet, there wouldn't be much point in returning it. But maybe it would be clearer and simpler to return everything. What do you think? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think returning everything might not be a good idea. Since the lifetime of toggles is a year, we might have many (>10 even) toggles in place that are turned off but still available. IMO, returning all of them would confuse the user. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In my opinion, the only time this header might get looked at by a developer is if there is a problem. In that case, I would probably like to have the whole picture. Ideally, there would also be something like As for the confusion bit, well toggles are going to add another layer of complexity for understanding what is happening with the API. Hiding them, would in this case probably provide more confusion, because a turned off toggle, which should maybe be on can be valuable information during debugging. |
||
|
||
- A feature toggle should have metadata such as a description, | ||
an expiration date, developer contact information, etc. | ||
|
||
- A feature toggle should have an optional version number, so | ||
you can get different versions of the same feature. | ||
|
||
- It should be possible to configure a toggle in `application.conf` | ||
so that its setting cannot be overridden per request. | ||
|
||
## Design | ||
|
||
### Configuration | ||
|
||
### Base Configuration | ||
|
||
The base configuration of feature toggles is in `application.conf` | ||
under `app.feature-toggles`. Example: | ||
|
||
``` | ||
app { | ||
feature-toggles { | ||
new-foo { | ||
description = "Replace the old foo routes with new ones." | ||
|
||
available-versions = [ 1, 2 ] | ||
default-version = 1 | ||
enabled-by-default = yes | ||
override-allowed = yes | ||
|
||
expiration-date = "2021-12-01T00:00:00Z" | ||
|
||
developer-emails = [ | ||
"A developer <a.developer@example.org>" | ||
] | ||
} | ||
|
||
fast-bar { | ||
description = "Replace the slower, more accurate bar route with a faster, less accurate one." | ||
|
||
available-versions = [ 1 ] | ||
default-version = 1 | ||
enabled-by-default = no | ||
override-allowed = yes | ||
|
||
developer-emails = [ | ||
"A developer <a.developer@example.org>" | ||
] | ||
} | ||
} | ||
} | ||
``` | ||
|
||
All fields are required except `expiration-date`. Each feature toggle must have | ||
at least one version number. Version numbers must be an ascending sequence of consecutive | ||
integers starting from 1. | ||
|
||
If `expiration-date` is provided, it must be an [`xsd:dateTimeStamp`](http://www.datypic.com/sc/xsd11/t-xsd_dateTimeStamp.html). All feature toggles | ||
should have expiration dates except for long-lived ops toggles like `fast-bar` above. | ||
|
||
`KnoraSettingsFeatureFactoryConfig` reads this base configuration on startup. If | ||
a feature toggle has an expiration date in the past, the application will not start. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. oh, this is a hard one. I wouldn't punish ops for developers not removing a toggle in time ;-) Also what should then happen, if the expiration date is passed while the app is deployed into production. I hope the app will not stop working? I think a log output would be sufficient. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I changed it to:
Just kidding. 😃 |
||
|
||
### Per-Request Configuration | ||
|
||
A client can override the base configuration by submitting the HTTP header | ||
`X-Knora-Feature-Toggles`. Its value is a comma-separated list of | ||
toggles. Each toggle consists of: | ||
|
||
1. its name | ||
2. a colon | ||
3. the version number | ||
4. an equals sign | ||
5. a boolean value, which can be `on`/`off`, `yes`/`no`, or `true`/`false` | ||
|
||
Using `on`/`off` is recommended for clarity. For example: | ||
|
||
``` | ||
X-Knora-Feature-Toggles: new-foo:2=on,fast-bar:1=on | ||
``` | ||
|
||
A version number must be given when enabling a toggle. | ||
It is an error to specify a version number when disabling | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. same as above! |
||
a toggle. | ||
|
||
## Response Header | ||
|
||
Knora API v2 and admin API responses contain the header | ||
`X-Knora-Feature-Toggles-Enabled`, whose value is a comma-separated, | ||
unordered list of toggles that are enabled. The response to the | ||
example above would be: | ||
|
||
``` | ||
X-Knora-Feature-Toggles-Enabled: new-foo:2,fast-bar:1 | ||
``` | ||
|
||
## Implementation Framework | ||
|
||
A `FeatureFactoryConfig` reads feature toggles from some | ||
configuration source, and optionally delegates to a parent | ||
`FeatureFactoryConfig`. | ||
|
||
`KnoraRoute` constructs a `KnoraSettingsFeatureFactoryConfig` | ||
to read the base configuration. For each request, it | ||
constructs a `RequestContextFeatureFactoryConfig`, which | ||
reads the per-request configuration and has the | ||
`KnoraSettingsFeatureFactoryConfig` as its parent. | ||
It then passes the per-request configuration object to the `makeRoute` | ||
method, which can in turn pass it to a feature factory, | ||
or send it in a request message to allow a responder to | ||
use it. | ||
|
||
### Feature Factories | ||
|
||
The traits `FeatureFactory` and `Feature` are just tagging traits, | ||
to make code clearer. The factory methods in a feature | ||
factory will depend on the feature, and need only be known by | ||
the code that uses the feature. The only requirement is that | ||
each factory method must take a `FeatureFactoryConfig` parameter. | ||
|
||
To get a `FeatureToggle`, a feature factory | ||
calls `featureFactoryConfig.getToggle`, passing the name of the toggle. | ||
If a feature toggle has only one version, it is enough to test | ||
whether test if the toggle is enabled, by calling `isEnabled` on the toggle. | ||
|
||
If the feature toggle has more than one version, call its `getMatchableState` | ||
method. To allow the compiler to check that matches on version numbers | ||
are exhaustive, this method is designed to be used with a sealed trait | ||
(extending `Version`) that is implemented by case objects representing | ||
the feature's version numbers. The method returns an instance of | ||
`MatchableState`, which is analogous to `Option`: it is either `Off` | ||
or `On`, and an instance of `On` contains one of the version objects. | ||
For example: | ||
|
||
``` | ||
// A trait for version numbers of the new 'foo' feature. | ||
sealed trait NewFooVersion extends Version | ||
|
||
// Represents version 1 of the new 'foo' feature. | ||
case object NEW_FOO_1 extends NewFooVersion | ||
|
||
// Represents version 2 of the new 'foo' feature. | ||
case object NEW_FOO_2 extends NewFooVersion | ||
|
||
// The old 'foo' feature implementation. | ||
private val oldFoo = new OldFooFeature | ||
|
||
// The new 'foo' feature implementation, version 1. | ||
private val newFoo1 = new NewFooVersion1Feature | ||
|
||
// The new 'foo' feature implementation, version 2. | ||
private val newFoo2 = new NewFooVersion2Feature | ||
|
||
def makeFoo(featureFactoryConfig: FeatureFactoryConfig): Foo = { | ||
// Get the 'new-foo' feature toggle. | ||
val fooToggle: FeatureToggle = featureFactoryConfig.getToggle("new-foo") | ||
|
||
// Choose an implementation according to the toggle state. | ||
fooToggle.getMatchableState(NEW_FOO_1, NEW_FOO_2) match { | ||
case Off => oldFoo | ||
case On(NEW_FOO_1) => newFoo1 | ||
case On(NEW_FOO_2) => newFoo2 | ||
} | ||
} | ||
``` | ||
|
||
### Routes as Features | ||
|
||
To select different routes according to a feature toggle: | ||
|
||
- Make a feature factory that extends `KnoraRouteFactory` and `FeatureFactory`, | ||
and has a `makeRoute` method that returns different implementations, | ||
each of which extends `KnoraRoute` and `Feature`. | ||
|
||
- Make a façade route that extends `KnoraRoute`, is used in | ||
`ApplicationActor.apiRoutes`, and has a `makeRoute` method that | ||
delegates to the feature factory. | ||
|
||
To avoid constructing redundant route instances, each façade route needs its | ||
own feature factory class. | ||
|
||
### Documenting a Feature Toggle | ||
|
||
The behaviour of each possible setting of each feature toggle should be | ||
documented. Feature toggles that are configurable per request should be described | ||
in the release notes. | ||
|
||
### Removing a Feature Toggle | ||
|
||
To facilitate removing a feature toggle, each implementation should have: | ||
|
||
- a separate file for its source code | ||
|
||
- a separate file for its documentation | ||
|
||
When the toggle is removed, the files that are no longer needed can be | ||
deleted. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
very well explained!