8000 chore(flags): Add method to build up evaluation levels from a flags DAG by haacked · Pull Request #33699 · PostHog/posthog · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

chore(flags): Add method to build up evaluation levels from a flags DAG #33699

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

Open
wants to merge 3 commits into
base: haacked/29016-more-refactorings
Choose a base branch
from

Conversation

haacked
Copy link
Contributor
@haacked haacked commented Jun 14, 2025

Note that this PR targets the PR #33643.

Problem

In preparation for the upcoming feature where feature flags can depend on other feature flags, we need to consider feature flag dependencies. Typically, this would require that we sort flags topologically, reverse them, and then evaluate them sequentially.

However, our current code evaluates flags in parallel, and we'd like to keep doing that. So what we need to do is build up a multi-root DAG (Directed Acyclic Graph) and evaluate flag dependencies in stages. This PR implements the ability to build those stages (it doesn't do the evaluation in stages yet).

Related to #29016

Changes

This adds a two new methods: build_from_nodes which takes in a collection of nodes and builds up a multi-root graph with all the nodes.

The second new method is evaluation_stages. This returns flags in stages where each stage must be evaluated before the next stage can be evaluated. Here's an example:

Suppose we have these flags (represented by IDs) and their dependencies (represented by arrows):

1
2 -> 3 -> 4
3 -> 4
4
5
6
7 -> 8
8

In other words:

1, 5, 6 - are independent flags (no dependencies, nobody depends on them)
2 depends on 3
3 depends on 4

When we evaluate these flags, we have to do them in the following stages:

  1. [1, 4, 5, 6, 8] (leaf nodes and indpendent flags)
  2. [3, 7] (flags with one dependency)
  3. [2] (flags with two dependencies)

This allows us to evaluate the flags in each stage in parallel while making sure we always evaluate dependencies before parents.

Did you write or update any docs for this change?

How did you test this code?

Unit tests

@haacked haacked requested review from Copilot, dmarticus and a team June 14, 2025 02:46
@dmarticus dmarticus moved this to In Review in Feature Flags Jun 14, 2025
Copy link
Contributor
@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

PR Summary

Implements parallel evaluation of feature flags using a dependency-based staging system, moving from sequential topological sorting to a more efficient approach.

  • Added build_from_nodes method to construct multi-root DAGs from feature flag collections with strict cycle validation
  • Implemented evaluation_stages to organize flags into parallel evaluation stages (e.g., [1, 4, 5, 6, 8] -> [3, 7] -> [2])
  • Reuses existing dependency graph infrastructure from cohort system for feature flags
  • Comprehensive test coverage for various dependency scenarios including linear chains and complex DAGs

2 files reviewed, no comments
Edit PR Review Bot Settings | Greptile

Copy link
Contributor
@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR adds graph construction and staged evaluation features to support dependent feature flags without changing the parallel evaluation model.

  • Introduces build_from_nodes to create a multi-root DAG from any set of flag nodes.
  • Adds evaluation_stages to batch nodes into dependency-respecting stages for parallel evaluation.
  • Refactors new to collect reachable nodes and delegate to build_from_nodes.

Reviewed Changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
graph_utils.rs Implemented build_from_nodes and evaluation_stages, refactored new.
test_graph_utils.rs Added unit tests for build_from_nodes and evaluation_stages.
Comments suppressed due to low confidence (1)

rust/feature-flags/src/utils/graph_utils.rs:75

  • The doc comment above new still refers to initial_item and items. Please update it to mention root and pool, and note that it delegates to build_from_nodes for consistency.
pub fn new(root: T, pool: &[T]) -> Result<Self, T::Error> {

Comment on lines +126 to +127
if let Some(target_idx) = id_map.get(&dep_id) {
graph.add_edge(source_idx, *target_idx, ());
Copy link
Preview
Copilot AI Jun 14, 2025

Choose a reason for hiding this comment

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

build_from_nodes silently drops dependencies that aren’t in the provided node list. It s 10000 hould return an error when dep_id is not found rather than ignoring it, to avoid hidden missing-dependency bugs.

Suggested change
if let Some(target_idx) = id_map.get(&dep_id) {
graph.add_edge(source_idx, *target_idx, ());
match id_map.get(&dep_id) {
Some(target_idx) => {
graph.add_edge(source_idx, *target_idx, ());
}
None => {
return Err(FlagError::MissingDependency(
T::dependency_type(),
dep_id.into(),
)
.into());
}

Copilot uses AI. Check for mistakes.

Ok(node)
if current_stage.is_empty() {
// This should never happen if the graph is acyclic (already validated during build)
return Err(FlagError::DependencyCycle(T::dependency_type(), 0).into());
Copy link
Preview
Copilot AI Jun 14, 2025

Choose a reason for hiding this comment

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

Using the literal 0 as the cycle-start ID is a magic number and may not correspond to a real node. Consider propagating the actual node ID or using a more descriptive error variant.

Suggested change
return Err(FlagError::DependencyCycle(T::dependency_type(), 0).into());
if let Some((&node_idx, _)) = out_degree.iter().next() {
let node_id = self.get_node_id(node_idx);
return Err(FlagError::DependencyCycle(T::dependency_type(), node_id).into());
}
// Fallback in case no node is found (should not occur)
return Err(FlagError::DependencyCycle(T::dependency_type(), -1).into());

Copilot uses AI. Check for mistakes.

pub fn new(initial_item: T, items: &[T]) -> Result<Self, T::Error> {
let mut graph = DiGraph::new();
let mut node_map = NodeMap::<T>::new();
pub fn new(root: T, pool: &[T]) -> Result<Self, T::Error> {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Because we have build_from_nodes we can make this method simpler by building on that.
Perhaps we should rename this method though?

@haacked haacked force-pushed the haacked/29016-evaluate-flags-with-dependencies branch from 1eb0013 to 778649a Compare June 14, 2025 03:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: In Review
Development

Successfully merging this pull request may close these issues.

1 participant
0