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

Dependent features #176

Closed
wants to merge 16 commits into from
Closed

Dependent features #176

wants to merge 16 commits into from

Conversation

Tymek
Copy link
Member

@Tymek Tymek commented Sep 27, 2023

Edit: Moved this into draft so can iterate a little on it

Description

Related to Unleash/unleash#2255

Introducing concept of "dependency" on a feature toggle. Feature toggle, in order to be enabled an return a variant, can require the specified state of another feature toggle. There will be a dependencies property on each feature.

{
 ...
 "dependencies": {
   "feature": "string-featureId",
   "enabled: true, // boolean, state that is expected defaults to `true`
   "variants": ["variant1", "variant2"] // optional, variants allowed for feature to be enabled
 }
}

Type of change

Please delete options that are not relevant.

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update

How Has This Been Tested?

Please describe the tests that you ran to verify your changes.

  • Unit tests
  • Spec Tests
  • Integration tests / Manual Tests

Checklist:

  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • Any dependent changes have been merged and published in downstream modules

@Tymek Tymek marked this pull request as ready for review October 2, 2023 16:24
@Tymek Tymek requested a review from kwasniew October 3, 2023 07:16
@@ -59,21 +63,157 @@ public function isEnabled(string $featureName, ?Context $context = null, bool $d
}
}

return $this->isFeatureEnabled($feature, $context, $default)->isEnabled();
return $this->isFeatureEnabled($feature, $context, $dependencies, $default)->isEnabled();
Copy link

Choose a reason for hiding this comment

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

why can't dependencies be taken from the feature and need to be passed as arguments

}

$dependencyVariants = $dependency->getVariants();
if (!empty($dependencyVariants)) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Please don't ever use empty(), that does like a million things. Be specific, you probably want count() instead of empty().

Copy link
Member

Choose a reason for hiding this comment

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

Me: Arrives on this pull request
Me: Reads this comment
Me: Can empty() really be that bad?
Me: Reads for 5 minutes
Me: Please don't ever use empty(), that does like a million things

@@ -43,7 +45,9 @@ public function __construct(
public function isEnabled(string $featureName, ?Context $context = null, bool $default = false): bool
{
$context ??= $this->configuration->getContextProvider()->getContext();
$feature = $this->findFeature($featureName, $context);
$featureAndDependencies = $this->findFeatureAndDependencies($featureName, $context);
Copy link

Choose a reason for hiding this comment

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

can findFeature return feature with dependencies same way as our API returns feature with dependencies?

Copy link
Member

Choose a reason for hiding this comment

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

I think we're thinking the same thing here, I have a similar comment on materializing this from cache in the correct DTO format. I think we're saying the same thing. If we are, I definitely agree

foreach ($dependencies as $dependency) {
$parentFeature = $parentFeatures[$dependency->getFeature()] ?? null;
if (!$this->isDependencySatisfied($dependency, $parentFeature, $context)) {
$this->metricsHandler->handleMetrics($feature, false);
Copy link

Choose a reason for hiding this comment

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

this reminds me to add a test that verifies that metrics are not counted for the parent features


return [
'feature' => null,
'dependencies' => [],
Copy link

Choose a reason for hiding this comment

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

actualDependencies

@Tymek
Copy link
Member Author

Tymek commented Oct 3, 2023

Can we somehow solve it in another way? Right now the issue is that it's not possible to get feature toggles from repository multiple times in multiple places in main class. This should be doable by holding in-memory copy of the response or state of the Repository, since it's already a property on main class. Am I missing something?
@sighphyre @RikudouSage

@RikudouSage
Copy link
Collaborator

@Tymek I'm not entirely sure what this feature is about (no one told me), but it seems that features now can have a dependency on another feature. If any parent feature evaluates to false, all child features are also false. Is that a correct assumption?

In that case, why not construct it recursively? The DTOs pretty much should be the JSON response but as PHP DTOs.

There's also the method findFeature(), why can't you use that to fetch the toggles multiple times?

If you describe the issue in more detail, I might be able to assist further.

@RikudouSage RikudouSage self-requested a review October 3, 2023 12:47
* dependencies: array<string, Feature>,
* }
*/
public function findFeatureAndDependencies(string $featureName, ?Context $context): ?array
Copy link
Member

Choose a reason for hiding this comment

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

Does this have to be public?

Also, I'm not sure this is the right layer to be doing this in. Not hitting the cache twice? Yes! Doing this... less yes.

Can we not materialise this from cache in a more complete state rather? I want to say the parseFeatures method in the repository is a good target for this. That can return a DTO that looks something like (in very pseudocode)

DefaultFeature {
name: String,
parent?: DefaultFeature
}

* dependencies: array<string, Feature>,
* }
*/
public function findFeatureAndDependencies(string $featureName, ?Context $context): ?array
Copy link
Member

Choose a reason for hiding this comment

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

Also, I don't think this method should take context. It's strange. It doesn't affect the outcome in any way outside of meta data in the events. Especially for a function marked public that feels off

@@ -83,9 +90,9 @@ public function findFeature(string $featureName): ?Feature
* @throws ClientExceptionInterface
* @throws JsonException
*
* @return iterable<Feature>
* @return array<Feature>
Copy link
Member

Choose a reason for hiding this comment

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

I see we're shifted from an iterable to an array in a few places in this PR but I can't see why we would need to. What's happening here?

Copy link
Member

@sighphyre sighphyre left a comment

Choose a reason for hiding this comment

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

I don't believe it's enough to rely on the client specs here. This is feature that can go wrong in a lot of subtle ways, even though it's simple on the surface. The PHP implementation is always going to have details that most of the other SDK implementations don't - you know the details better than anyone, cover them with tests

If not to make me happy, then to make the quality gates on this repo happy

Copy link
Member

@sighphyre sighphyre left a comment

Choose a reason for hiding this comment

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

Can we also put a proper description on this PR? This is a significant feature and it's helpful for others to understand what it does, why it does it and how it works. Especially for reviewers who are seeing this feature for the first time

@sighphyre
Copy link
Member

In that case, why not construct it recursively? The DTOs pretty much should be the JSON response but as PHP DTOs.

I think we can and should do that!

There's also the method findFeature(), why can't you use that to fetch the toggles multiple times?

findFeature() will load and parse everything from cache every time, it's expensive(ish) with large feature sets. I'd rather avoid it if we can. If it's hard to do, I'm happy to go with better, more understandable code over fast code. But I think just constructing the DTOs in the repo layer recursively would give us nice code that's also cheap

* refactor: load dependencies when parsing

* fix: phpstan errors

* fix: variant hash seed

* chore: update client specification

* fix: stickiness calculator type

* fix: variant selection

* fix: finding dependency
@Tymek Tymek requested a review from sighphyre October 31, 2023 12:22
@sighphyre sighphyre marked this pull request as draft November 8, 2023 09:14
@RikudouSage RikudouSage mentioned this pull request Nov 24, 2023
11 tasks
@RikudouSage
Copy link
Collaborator

Replaced with #187

@RikudouSage RikudouSage deleted the feat/dependent-features branch January 12, 2024 11:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Archived in project
Development

Successfully merging this pull request may close these issues.

None yet

4 participants