open-feature / open-feature/protocol
OFREP ETag can result in race condition weakness.
Nobody has claimed this yet.
- Dominant language
- No language data
- Stars
- 56
- Forks
- 7
- Avg merge
- 1h 54m
- Merged PRs (30d)
- 1
Description
Problem
By using the ETag as the cache differentiator, you can run into some ugly consequences based on the reliability/consistency of your underlying datastore. This is not specific to any one implementation or another.
Detail
ETag just assigns whether or not the value has changed since the last request, and has no bearing on when, or the order of what lifecycle operation configs have been changed. This can have major impacts, as illustrated later on.
Below is a sample progression of the versions of a single configuration that were made extremely close together. In this case, each configuration is different (hence the different ETags) and was updated in sequence.
flowchart LR
CN[Config 0<br/>ETag=Z]
C1[Config 1<br/>ETag=A]
C2[Config 2<br/>ETag=B]
C3[Config 3<br/>ETag=C]
CP[Config N...]
CN -->|T=-2...| C1
C1 -->|T=0.1| C2
C2 -->|T=0.2| C3
C3 -->|T=0.3| CP
This update process means that you are relying on your backing datastore to be guaranteed to write the configurations in order, every time, and ensure that there are no out of order writes.
If there are out of order writes (or another race condition in something persay the cache filling, etc) you may get an unknown final configuration at T=0.5.
For the purposes of this, the reason (SSE, Polling, etc) for the given refetches is irrelevant as they're a complexity that can make this problem worse, or better depending on vendor implementation. The OFREP spec does not define how these messages should be handled/sent out based on multiple changes to the underlying configuration, and thus we cannot speculate on a given design.
Example Issues
Example 1 - Not an issue
Let's pose a situation here with the following criteria:
- An OFREP provider has been initialized with
Config 0, an arbitrary configuration before the series of edits here, with an ETag ofZ. - There was a race condition in the underlying store for the configuration, and the configuration being served is
Config 2incorrectly. - The OFREP provider makes a fetch to retrieve the current config at
T=0.35sending theIf-None-Match=Zand retrievesConfig 2.
This results in the provider comparing the ETags and validating them to be different, and accepting Config 2 as the valid config. For all intents and purposes to the provider, this is accurate and has no issues. But this means the provider has missed the latest available update. Not the fault of OFREP here.
Example 2 - Unintended Rollback of the config
Let's add a small modification to this:
- An OFREP provider has been initialized with
Config 2, as it initialized/fetched the config at the exact moment it was written, with an ETag ofB. - There was a race condition in the underlying store for the configuration, and shortly after initialization the final config written resulted in being
Config 1.
This has a much more drastic impact. To the provider, we're now invalidating the current configuration with an invalid out of date one. But because the ETag is "different" there is no reason to suspect/reject the configuration.
The reality here, is that the configuration was rolled back in time, with potentially unknown impact based on the usage/plan to roll out the config.
Proposed Solution - LastModified Headers
By introducing the LastModified Header as an optional/equally valid cache invalidation header option, you now add the timestamp of the originating file/change to the validation path.
Let's revisit the previous situations but with the concept of validating against a LastModified Header.
flowchart LR
CN[Config 0<br/>LastModified=T<br/>ETag=Z]
C1[Config 1<br/>LastModified=T+0.1<br/>ETag=A]
C2[Config 2<br/>LastModified=T+0.2<br/>ETag=B]
C3[Config 3<br/>LastModified=T+0.3<br/>ETag=C]
CP[Config N...]
CN -->|T=-2...| C1
C1 -->|T=0.1| C2
C2 -->|T=0.2| C3
C3 -->|T=0.3| CP
Example 1 - Not an issue
Let's pose a situation here with the following criteria:
- An OFREP provider has been initialized with
Config 0, an arbitrary configuration before the series of edits here, with a LastModified ofT. - The OFREP provider makes a fetch to retrieve the current config at
T=0.35with aIf-Modified-Since=Theader and retrieves Config 2 - There was a race condition in the underlying store for the configuration, and the configuration being served is
Config 2incorrectly
This is valid, and does not drastically impact differently than the ETag situation.
Example 2 - Unintended Rollback of the config
- An OFREP provider has been initialized with
Config 2, as it initialized/fetched the config at the exact moment it was written, with a LastModified ofT+0.2. - There was a race condition in the underlying store for the configuration, and shortly after initialization the final config written resulted in being
Config 1. - The provider fetches for the configuration with an
If-Modified-Since=T+0.2header and receives a 304, and does not update the config.
Proposal
This ultimately is a safer, and more stable way of returning configuration which at its core is naturally a timeseries based datatype.
The recommendation here is to expand the OFREP spec to make the LastModified header an equally valid cache invalidation header to the bulk evaluation endpoint.
This is a defense in depth approach, and is something very real that I've personally had to deal with and solve for with the DevCycle SDKs and Cloudflare's cache not properly being globally invalidated in time when relating to successive changes.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by reading the OFREP specification for the bulk evaluation endpoint and its current ETag cache-invalidation behavior. The proposal is complete when the specification's handling of LastModified headers is decided and documented, including how it relates to successive configuration changes and conditional requests.
Written by the indexing model from the issue text.
Assessment
- Domain
- api
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Needs clarification
- Newbie friendliness
- 30/100