Skip to content
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

Merged
merged 46 commits into from Nov 5, 2020
Merged
Show file tree
Hide file tree
Changes from 38 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
fcd889f
feat: Add draft abstractions for feature toggles.
Oct 26, 2020
53f9d4a
style: Fix typo.
Oct 26, 2020
60498d6
feat: Add draft feature toggle design.
Oct 27, 2020
56585b9
style: Fix typo.
Oct 27, 2020
0eac91c
Merge branch 'develop' into wip/DSP-910-feature-toggle
Oct 27, 2020
e09b428
Merge branch 'develop' into wip/DSP-910-feature-toggle
Oct 30, 2020
22b1d7a
feat(feature-toggles): Add design TODOs.
Oct 30, 2020
2e1cde5
feat(feature-toggles): Add metadata, versions, and override-allowed.
Nov 2, 2020
de2bf86
feat(feature-toggles): Support feature toggles in all routes.
Nov 2, 2020
e2d95a4
fix(sipi): Fix inheritance.
Nov 2, 2020
d6985b1
style(feature-toggles): Clarify and simplify code.
Nov 3, 2020
ba4dfe9
test(feature-toggles): Add tests.
Nov 3, 2020
d9f8f6e
Merge branch 'main' into wip/DSP-910-feature-toggle
Nov 3, 2020
8159d51
docs(feature-toggles): Add docs.
Nov 3, 2020
69d1336
docs(feature-toggles): Describe how to remove feature toggles.
Nov 3, 2020
a7e2dd0
docs(feature-toggles): Clarify version numbers.
Nov 3, 2020
e8d1464
docs(feature-toggles): Fix typo.
Nov 3, 2020
8876dc1
refactor(KnoraRoute): Clarify and simplify code.
Nov 3, 2020
e83eb06
refactor(KnoraRoute): Clarify code.
Nov 3, 2020
4823756
style(feature-toggles): Add comments.
Nov 3, 2020
b2fdc71
refactor(feature-toggles): Remove unnecessary val.
Nov 3, 2020
9f43b94
refactor(feature-toggles): Remove unnecessary val.
Nov 3, 2020
dfa805f
style(feature-toggles): Simplify code.
Nov 3, 2020
18134ab
style(feature-toggles): Simplify code.
Nov 3, 2020
e341ffa
style(feature-toggles): Use drop(1) instead of tail.
Nov 3, 2020
830065c
Merge branch 'main' into wip/DSP-910-feature-toggle
Nov 4, 2020
5281984
docs(feature-toggles): Add link.
Nov 4, 2020
8392570
style(feature-toggles): Clarify comment.
Nov 4, 2020
f216e03
feat(feature-toggles): Reject version numbers less than 1.
Nov 4, 2020
f4c5733
style(feature-toggles): Simplify code.
Nov 4, 2020
c2008e0
feat(feature-toggles): Improve error checking.
Nov 4, 2020
2918768
style(feature-toggles): Add comment.
Nov 4, 2020
d8fedca
fix(feature-toggles): Don't sort version numbers, they have to be sor…
Nov 4, 2020
e40fb84
feat(feature-toggles): Improve error checking.
Nov 4, 2020
09cd8fe
feat(feature-toggles): Require a version number when enabling a toggle.
Nov 4, 2020
e5b7404
refactor(feature-toggles): Refactor header parsing.
Nov 4, 2020
f17d89b
refactor(feature-toggles): Remove unnecessary implicit.
Nov 4, 2020
4f44afe
style(feature-toggles): Reorder cases.
Nov 4, 2020
3bb2024
feat(feature-toggles): Redesign version matching.
Nov 4, 2020
bb55982
feat(feature-toggles): Improve error handling.
Nov 4, 2020
27de670
style(feature-toggles): Clarify code.
Nov 4, 2020
9fe4429
style(KnoraSettings): Add newline.
Nov 4, 2020
5474705
docs(feature-toggles): Clarify docs, add tests.
Nov 4, 2020
4c94f6e
docs(feature-toggles): Clarify some more.
Nov 4, 2020
cbe3885
docs(feature-toggles): Clarify some more.
Nov 4, 2020
6b34321
feat(feature-toggles): Return all toggles in response header.
Nov 5, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
75 changes: 75 additions & 0 deletions docs/03-apis/feature-toggles.md
@@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

very well explained!


Some Knora features can be turned or off on a per-request basis.
Copy link
Collaborator

Choose a reason for hiding this comment

The 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
Copy link
Contributor

Choose a reason for hiding this comment

The 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
```
2 changes: 2 additions & 0 deletions docs/03-apis/index.md
Expand Up @@ -29,3 +29,5 @@ The Knora APIs include:
administering projects that use Knora as well as Knora itself.
* The Knora [Util API](api-util/index.md), which is intended to be used for information retrieval
about the Knora-stack itself.

Knora API v2 and the admin API support [Feature Toggles](feature-toggles.md).
2 changes: 1 addition & 1 deletion docs/05-internals/design/api-v2/how-to-add-a-route.md
Expand Up @@ -64,7 +64,7 @@ See the routes in that package for examples. Typically, each route
route will construct a responder request message and pass it to
`RouteUtilV2.runRdfRouteWithFuture` to handle the request.

Finally, add your `knoraApiPath` function to the `apiRoutes` member
Finally, add your route's `knoraApiPath` function to the `apiRoutes` member
variable in `KnoraService`. Any exception thrown inside the route will
be handled by the `KnoraExceptionHandler`, so that the correct client
response (including the HTTP status code) will be returned.
249 changes: 249 additions & 0 deletions docs/05-internals/design/principles/feature-toggles.md
@@ -0,0 +1,249 @@
<!---
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.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the response header also going to return turned off toggles?

Copy link
Author

Choose a reason for hiding this comment

The 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?

Copy link
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Collaborator

Choose a reason for hiding this comment

The 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 x-knora-api-version. Those two headers would then paint the whole picture (in combination with a table in the documentation).

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.
Copy link
Collaborator

Choose a reason for hiding this comment

The 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.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed it to:

the application will log the error message "SHOW-STOPPER!!!" and the server will then self-destruct

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
Copy link
Contributor

Choose a reason for hiding this comment

The 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.
To test if the toggle is enabled, call `isEnabled` on the toggle.

To get the configured version of the toggle, call its `checkVersion`
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. 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 = {
// Is the 'new-foo' feature toggle enabled?
val fooToggle: FeatureToggle = featureFactoryConfig.getToggle("new-foo")

if (fooToggle.isEnabled) {
// Yes. Which version is enabled?
fooToggle.checkVersion(NEW_FOO_1, NEW_FOO_2) match {
case NEW_FOO_1 =>
// Version 1.
newFoo1

case NEW_FOO_2 =>
// Version 2.
newFoo2
}
} else {
// No, the feature is disabled. Use the old implementation.
oldFoo
}
}
```

### 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.
1 change: 1 addition & 0 deletions docs/05-internals/design/principles/index.md
Expand Up @@ -26,3 +26,4 @@ License along with Knora. If not, see <http://www.gnu.org/licenses/>.
- [Triplestore Updates](triplestore-updates.md)
- [Consistency Checking](consistency-checking.md)
- [Authentication](authentication.md)
- [Feature Toggles](feature-toggles.md)
1 change: 1 addition & 0 deletions third_party/dependencies.bzl
Expand Up @@ -153,6 +153,7 @@ ALL_WEBAPI_MAIN_DEPENDENCIES = [
"//webapi/src/main/scala/org/knora/webapi/app",
"//webapi/src/main/scala/org/knora/webapi/core",
"//webapi/src/main/scala/org/knora/webapi/exceptions",
"//webapi/src/main/scala/org/knora/webapi/feature",
"//webapi/src/main/scala/org/knora/webapi/http/handler",
"//webapi/src/main/scala/org/knora/webapi/http/version",
"//webapi/src/main/scala/org/knora/webapi/http/version/versioninfo",
Expand Down
17 changes: 17 additions & 0 deletions webapi/src/main/resources/application.conf
Expand Up @@ -264,6 +264,23 @@ akka-http-cors {
}

app {
feature-toggles {
new-list-admin-routes {
description = "Replace the old list admin routes with new ones."

available-versions = [ 1 ]
default-version = 1
enabled-by-default = no
override-allowed = no
expiration-date = "2021-12-01T00:00:00Z"

developer-emails = [
"Sepideh Alassi <sepideh.alassi@dasch.swiss>"
"Benjamin Geer <benjamin.geer@dasch.swiss>"
]
}
}

print-extended-config = false // If true, an extended list of configuration parameters will be printed out at startup.
print-extended-config = ${?KNORA_WEBAPI_PRINT_EXTENDED_CONFIG}

Expand Down