Skip to content

init rate rollouts #459

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 4 commits into
base: main
Choose a base branch
from
Open

init rate rollouts #459

wants to merge 4 commits into from

Conversation

jsbroks
Copy link
Member

@jsbroks jsbroks commented Apr 6, 2025

Summary by CodeRabbit

  • New Features

    • Introduced a new rollout control mechanism for deployments that gradually enables releases over time, providing clear feedback on rollout progress and availability.
    • Added a comprehensive suite of unit tests for the rollout rules, validating functionality across various scenarios.
  • Enhancements

    • Improved version tracking by including creation timestamps for releases, offering clearer insights into release histories.
    • Enhanced module export capabilities by making the rate-rollout-rule accessible from the main index.

Copy link
Contributor

coderabbitai bot commented Apr 6, 2025

Walkthrough

This pull request introduces a new rollout rule for deployments by implementing the RateRolloutRule class, which determines if releases are eligible based on a calculated rollout percentage using a deterministic hash and current time. Unit tests cover scenarios for complete rollout, partial rollout, and rejection reasoning using a fixed base date. Additionally, the module exports have been updated to include the new rule, and the release type has been enhanced with a createdAt timestamp.

Changes

File(s) Change Summary
packages/.../rules/{__tests__/rate-rollout-rule.test.ts, index.ts, rate-rollout-rule.ts} Introduces the RateRolloutRule class with its implementation, including rollout calculations and rejection reasons; adds unit tests (complete, partial, and rejection scenarios); and updates exports.
packages/.../types.ts Adds a createdAt: Date property to the version field in the ResolvedRelease type.
packages/.../rules/__tests__/deployment-deny-rule.test.ts Updates the test setup to include createdAt timestamps for the version objects in the sampleReleases array.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Rule as RateRolloutRule
    participant Clock
    Client->>Rule: filter(context, releases)
    Rule->>Clock: Retrieve current time
    Clock-->>Rule: Return fixed/current time
    Rule->>Rule: Iterate releases, calculate rollout %
    Rule->>Client: Return allowed releases & rejection reasons
Loading

Poem

I'm a rabbit with a happy code hop,
New rules in place—no chance to stop!
Time is mocked, releases roll by,
Testing each step as moments fly.
Through each change I bound with cheer,
Celebrating progress far and near. 🐇✨

✨ Finishing Touches
  • 📝 Generate Docstrings

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai plan to trigger planning for file edits and PR creation.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (3)
packages/rule-engine/src/rules/rate-rollout-rule.ts (2)

41-91: Consider clamping negative ages for safety.

If a release has a future timestamp, differenceInSeconds might yield a negative value. While unlikely, it could impact the rollout logic. Consider clamping to zero:

-const releaseAge = differenceInSeconds(now, releaseCreatedAt);
+const releaseAge = Math.max(0, differenceInSeconds(now, releaseCreatedAt));

This ensures the rollout process never calculates a negative percentage.


93-99: Optional fallback to release.createdAt if version date is absent.

Currently, getVersionCreatedAt always returns release.version.createdAt. If there's any chance version.createdAt might be undefined, consider a fallback:

-private getVersionCreatedAt(release: ResolvedRelease): Date {
-  return release.version.createdAt;
+private getVersionCreatedAt(release: ResolvedRelease): Date {
+  return release.version.createdAt ?? release.createdAt;
 }
packages/rule-engine/src/rules/__tests__/rate-rollout-rule.test.ts (1)

41-59: Well-designed helper function for creating test data

The createMockReleases function nicely abstracts the creation of test data, making the test cases cleaner and more readable.

Consider adding a comment explaining why both the release and version have the same creation date, and you might be able to remove the type assertion as ResolvedRelease if the object literal is properly typed.

 const createMockReleases = (releaseTimes: number[]): Releases => {
   const releases = releaseTimes.map((minutesAgo, index) => {
     const createdAt = new Date(baseDate.getTime() - minutesAgo * 60 * 1000);
+    // Both release and version share the same creation timestamp for testing simplicity
     return {
       id: `release-${index}`,
       createdAt,
       version: {
         id: `version-${index}`,
         tag: `v0.${index}.0`,
         config: {},
         metadata: {},
         createdAt,
       },
       variables: {},
-    } as ResolvedRelease;
+    };
   });

   return new Releases(releases);
 };
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 50a1a0e and 8af9807.

📒 Files selected for processing (4)
  • packages/rule-engine/src/rules/__tests__/rate-rollout-rule.test.ts (1 hunks)
  • packages/rule-engine/src/rules/index.ts (1 hunks)
  • packages/rule-engine/src/rules/rate-rollout-rule.ts (1 hunks)
  • packages/rule-engine/src/types.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
`**/*.{ts,tsx}`: **Note on Error Handling:** Avoid strict enforcement of try/catch blocks. Code may use early returns, Promise chains (.then().catch()), or other patterns for error...

**/*.{ts,tsx}: Note on Error Handling:
Avoid strict enforcement of try/catch blocks. Code may use early returns, Promise chains (.then().catch()), or other patterns for error handling. These are acceptable as long as they maintain clarity and predictability.

  • packages/rule-engine/src/types.ts
  • packages/rule-engine/src/rules/rate-rollout-rule.ts
  • packages/rule-engine/src/rules/__tests__/rate-rollout-rule.test.ts
  • packages/rule-engine/src/rules/index.ts
🧬 Code Definitions (2)
packages/rule-engine/src/rules/rate-rollout-rule.ts (1)
packages/rule-engine/src/types.ts (4)
  • DeploymentResourceRule (59-65)
  • DeploymentResourceContext (38-43)
  • DeploymentResourceRuleResult (45-48)
  • ResolvedRelease (7-18)
packages/rule-engine/src/rules/__tests__/rate-rollout-rule.test.ts (2)
packages/rule-engine/src/types.ts (1)
  • ResolvedRelease (7-18)
packages/rule-engine/src/rules/rate-rollout-rule.ts (1)
  • RateRolloutRule (23-113)
⏰ Context from checks skipped due to timeout of 90000ms (2)
  • GitHub Check: Lint
  • GitHub Check: build (linux/amd64)
🔇 Additional comments (14)
packages/rule-engine/src/rules/index.ts (1)

2-2: Exporting the new rate rollout rule.

This addition looks good. It cleanly exposes the new RateRolloutRule for external usage.

packages/rule-engine/src/types.ts (1)

15-15:

❓ Verification inconclusive

Verify the rationale for adding another createdAt.

Both the ResolvedRelease and its version object now have a createdAt property. If these timestamps can differ, this is valid. Otherwise, consider unifying to avoid confusion.

Below is a script to locate all references to createdAt in the codebase so you can confirm consistent usage:


🏁 Script executed:

#!/bin/bash
rg -A 5 'createdAt'

Length of output: 74147


Clarify the Distinct Roles for the Two createdAt Properties

It appears that in packages/rule-engine/src/types.ts the release object has its own createdAt property while the nested version object also includes a createdAt. Notably, functions such as the one in packages/rule-engine/src/rules/rate-rollout-rule.ts explicitly reference version.createdAt, suggesting that the version timestamp may represent a different event (for example, the time the version was defined) compared to the overall release creation time.

  • Release vs. Version Timestamp:
    • The top-level createdAt likely indicates when the release record was created.
    • The nested version.createdAt appears to capture when the version itself was established, and is actively used in the rate rollout logic.

Please verify that this distinction is intentional. If these two timestamps can indeed differ, the current implementation is valid. If they are intended to be identical, consider unifying them to avoid potential confusion.

packages/rule-engine/src/rules/rate-rollout-rule.ts (6)

1-2: Use of date-fns library is appropriate.

differenceInSeconds from date-fns is a clean approach for time-based calculations, and it keeps the code readable.


3-9: Proper import of type definitions.

The necessary interfaces and types are being imported to implement the DeploymentResourceRule. Everything looks consistent.


11-21: Clear interface definition for the rollout rule.

The rolloutDurationSeconds and denyReason properties provide flexibility. The documentation comments are concise and helpful.


23-34: Constructor sets up essential parameters.

Storing the rollout duration and the custom deny reason is straightforward. The default deny reason is a good fallback for cases when one isn't provided.


36-40: Good testability design with getCurrentTime.

Providing a dedicated method for retrieving the current time improves test control and reliability. No issues here.


100-112: Simple hash-based rollout logic.

This deterministic hashing approach is a quick way to generate a 0-100 range. For small IDs, collisions are unlikely problematic. No major concerns.

packages/rule-engine/src/rules/__tests__/rate-rollout-rule.test.ts (6)

1-6: Imports are well organized and appropriate

The test file correctly imports the necessary utilities from Vitest and the required components from the codebase.


7-22: Well-structured test context

The mock deployment context provides all the necessary properties that would be used by the RateRolloutRule class during testing, matching what would be expected by the rule's filter method.


24-39: Good time-based testing setup

The date mocking implementation is well-executed:

  • Using a fixed base date ensures consistent test results
  • Proper test hygiene with reset and restore operations in the appropriate hooks
  • The spying approach on the Date constructor is correct for controlling time during tests

61-79: Complete rollout test is well implemented

The test for complete rollout is thorough:

  • It correctly tests that releases created long ago (beyond the rollout period) are allowed
  • The hash value mocking ensures deterministic test results
  • Both allowed releases and rejection reasons are verified

Note: The test expects result.rejectionReasons to be undefined when all releases are allowed rather than an empty collection. This appears to be the intended behavior of the implementation.


81-129: Partial rollout test is comprehensive

This test thoroughly validates the partial rollout scenario:

  • It correctly simulates different release ages relative to the rollout window
  • The deterministic hash values are well-chosen to test boundary conditions
  • It properly verifies both allowed and rejected releases
  • The test correctly checks that rejection reasons exist for denied releases

131-159: Rejection reason test provides good coverage

This test effectively validates the format and content of rejection reasons:

  • It correctly forces a release to be denied with a high hash value
  • It verifies both the percentage completion and time remaining components
  • The regex pattern appropriately matches the expected time format

The test ensures users will receive clear, informative rejection messages with accurate timing estimates.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (2)
packages/rule-engine/src/rules/__tests__/rate-rollout-rule.test.ts (2)

41-59: Add test coverage for version creation date edge cases

Consider adding tests for scenarios where version.createdAt and release.createdAt differ, since the implementation in rate-rollout-rule.ts specifically uses version.createdAt for age calculation.

it("should use version.createdAt instead of release.createdAt for age calculation", () => {
  // Mock the Date constructor to return a fixed "now" time
  const now = new Date("2025-01-01T01:00:00Z"); // 1 hour from base
  dateSpy.mockImplementation(() => now);

  // Create a rule with a 2-hour rollout period
  const rule = new RateRolloutRule({
    rolloutDurationSeconds: 7200, // 2 hours
  });

  // Create a release where version.createdAt and release.createdAt differ
  const release = {
    id: "release-diff-dates",
    createdAt: new Date(now.getTime() - 30 * 60 * 1000), // 30 min ago (25%)
    version: {
      id: "version-diff-dates",
      tag: "v1.0.0",
      config: {},
      metadata: {},
      createdAt: new Date(now.getTime() - 90 * 60 * 1000), // 90 min ago (75%)
    },
    variables: {},
  } as ResolvedRelease;

  const releases = new Releases([release]);

  // Mock hash value to be between 25% and 75%
  vi.spyOn(rule as any, "getHashValue").mockReturnValue(50);

  const result = rule.filter(mockDeploymentContext, releases);

  // Should be allowed based on version.createdAt (75% > 50%), not release.createdAt
  expect(result.allowedReleases.length).toBe(1);
});

92-94: Test custom deny reason parameter

Consider adding a test case that verifies the custom deny reason parameter works correctly. The current tests only use the default deny reason.

it("should use custom deny reason when provided", () => {
  const now = new Date("2025-01-01T00:30:00Z");
  dateSpy.mockImplementation(() => now);
  
  const customDenyReason = "Custom deny reason for testing";
  const rule = new RateRolloutRule({
    rolloutDurationSeconds: 7200,
    denyReason: customDenyReason,
  });
  
  const releases = createMockReleases([10]);
  vi.spyOn(rule as any, "getHashValue").mockReturnValue(100);
  
  const result = rule.filter(mockDeploymentContext, releases);
  const rejectionReason = result.rejectionReasons?.get("release-0");
  
  expect(rejectionReason).toContain(customDenyReason);
});
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8af9807 and 3ca84c6.

📒 Files selected for processing (2)
  • packages/rule-engine/src/rules/__tests__/deployment-deny-rule.test.ts (2 hunks)
  • packages/rule-engine/src/rules/__tests__/rate-rollout-rule.test.ts (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • packages/rule-engine/src/rules/tests/deployment-deny-rule.test.ts
🧰 Additional context used
📓 Path-based instructions (1)
`**/*.{ts,tsx}`: **Note on Error Handling:** Avoid strict enforcement of try/catch blocks. Code may use early returns, Promise chains (.then().catch()), or other patterns for error...

**/*.{ts,tsx}: Note on Error Handling:
Avoid strict enforcement of try/catch blocks. Code may use early returns, Promise chains (.then().catch()), or other patterns for error handling. These are acceptable as long as they maintain clarity and predictability.

  • packages/rule-engine/src/rules/__tests__/rate-rollout-rule.test.ts
🧬 Code Definitions (1)
packages/rule-engine/src/rules/__tests__/rate-rollout-rule.test.ts (2)
packages/rule-engine/src/types.ts (1)
  • ResolvedRelease (7-18)
packages/rule-engine/src/rules/rate-rollout-rule.ts (1)
  • RateRolloutRule (23-113)
⏰ Context from checks skipped due to timeout of 90000ms (3)
  • GitHub Check: build (linux/amd64)
  • GitHub Check: Typecheck
  • GitHub Check: Lint
🔇 Additional comments (7)
packages/rule-engine/src/rules/__tests__/rate-rollout-rule.test.ts (7)

1-6: LGTM: Appropriate imports for testing the RateRolloutRule

The imports are well-organized, including necessary test utilities from Vitest and the required types and classes from the project.


7-22: LGTM: Well-structured mock deployment context

The mock deployment context provides all the necessary properties for testing the rule's filter method with a realistic context object.


24-39: LGTM: Effective date mocking setup

Good approach for ensuring deterministic date behavior in tests by:

  1. Setting a fixed base date
  2. Spying on the Date constructor
  3. Properly resetting mocks between tests
  4. Restoring all mocks after tests

This ensures reliable and repeatable test results for time-based functionality.


41-59: LGTM: Useful mock release creation helper

The createMockReleases helper function is well-designed:

  • Takes an array of "minutes ago" values for flexible test data creation
  • Generates proper timestamps relative to the base date
  • Creates complete release objects with all required properties
  • Returns a properly instantiated Releases collection

This approach reduces code duplication and makes tests more maintainable.


61-84: LGTM: Comprehensive test for complete rollout scenario

This test effectively verifies that releases beyond the rollout period are allowed:

  • Creates releases with timestamps far in the past
  • Sets up a short rollout period (10 minutes)
  • Mocks hash values deterministically
  • Properly verifies that all releases are allowed

The test covers an essential use case for the rollout rule.


86-146: LGTM: Thorough test for partial rollout behavior

This test case effectively validates the core functionality of the rule:

  • Sets up a specific "now" time one hour after the base date
  • Creates a 2-hour rollout period for testing partial rollout
  • Creates releases at various points in the rollout window (25%, 50%, 75%, 100%)
  • Mocks hash values to create deterministic test conditions
  • Verifies each release is correctly allowed or rejected based on its age and hash

The detailed assertions for each release ensure the rule's behavior is thoroughly tested.


148-176: LGTM: Appropriate test for rejection reasoning

This test effectively verifies the rejection reason formatting:

  • Creates a test environment with a specific "now" time
  • Forces a rejection by setting a high hash value
  • Verifies that the rejection reason includes the correct percentage (8%)
  • Checks that the remaining time is properly formatted (~1h Xm)

This ensures the rule provides helpful information to users when releases are rejected.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (2)
packages/rule-engine/src/rules/__tests__/rate-rollout-rule.test.ts (2)

79-100: Rejection test case could be enhanced with a more explicit expectation

The rejection test correctly verifies the behavior when a release should be rejected because its hash value exceeds the current rollout percentage. However, the calculation of the expected rejection reason is a bit complex inline.

Consider making the expected message calculation more explicit for better readability:

- const expectedRejectionReason = `Release denied due to rate-based rollout restrictions (${Math.round(
-   (nowSecondsAfterBase / rolloutDurationSeconds) * 100,
- )}% complete, eligible in ~5m)`;
+ const rolloutPercentage = Math.round((nowSecondsAfterBase / rolloutDurationSeconds) * 100);
+ const expectedRejectionReason = `Release denied due to rate-based rollout restrictions (${rolloutPercentage}% complete, eligible in ~5m)`;

8-101: Consider adding tests for additional edge cases

The current tests cover the main scenarios well, but there are some edge cases that could be tested for completeness.

Consider adding tests for:

  1. Zero rollout percentage (just started rollout period)
  2. Multiple releases with different creation dates
  3. Edge case where hash is 100 (maximum) with rollout percentage less than 100%

Example for testing zero rollout percentage:

it("should reject a release at the start of the rollout period", () => {
  const rule = new RateRolloutRule({ rolloutDurationSeconds: 600 });
  // Set current time equal to the base date (0% rollout)
  vi.spyOn(rule as any, "getCurrentTime").mockReturnValue(baseDate);
  // Any hash value > 0 should cause rejection
  vi.spyOn(rule as any, "getHashValue").mockReturnValue(1);

  const result = rule.filter(
    mockDeploymentContext,
    new Releases([mockRelease]),
  );

  expect(result.allowedReleases.getAll().length).toBe(0);
  expect(result.rejectionReasons.get(mockRelease.id)).toContain("0% complete");
});
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2588bd3 and 3839294.

📒 Files selected for processing (2)
  • packages/rule-engine/src/rules/__tests__/rate-rollout-rule.test.ts (1 hunks)
  • packages/rule-engine/src/rules/rate-rollout-rule.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/rule-engine/src/rules/rate-rollout-rule.ts
🧰 Additional context used
📓 Path-based instructions (1)
`**/*.{ts,tsx}`: **Note on Error Handling:** Avoid strict enforcement of try/catch blocks. Code may use early returns, Promise chains (.then().catch()), or other patterns for error...

**/*.{ts,tsx}: Note on Error Handling:
Avoid strict enforcement of try/catch blocks. Code may use early returns, Promise chains (.then().catch()), or other patterns for error handling. These are acceptable as long as they maintain clarity and predictability.

  • packages/rule-engine/src/rules/__tests__/rate-rollout-rule.test.ts
🧬 Code Definitions (1)
packages/rule-engine/src/rules/__tests__/rate-rollout-rule.test.ts (3)
packages/rule-engine/src/types.ts (1)
  • ResolvedRelease (7-18)
packages/rule-engine/src/rules/rate-rollout-rule.ts (1)
  • RateRolloutRule (23-112)
packages/rule-engine/src/releases.ts (1)
  • Releases (18-306)
⏰ Context from checks skipped due to timeout of 90000ms (3)
  • GitHub Check: build (linux/amd64)
  • GitHub Check: Lint
  • GitHub Check: Typecheck
🔇 Additional comments (4)
packages/rule-engine/src/rules/__tests__/rate-rollout-rule.test.ts (4)

1-7: Imports are well-organized

The file properly imports utilities from date-fns and vitest for testing, along with the necessary types and classes from the application codebase.


8-45: Test fixtures are well-defined

The test setup includes a clear mock deployment context, a fixed base date for deterministic testing, and proper test lifecycle hooks to reset mocks. The mock release object correctly matches the expected structure defined in the types.


47-61: Complete rollout test case looks good

This test correctly verifies that a release is allowed when its rollout period is complete (100% rollout). The mocking approach for getCurrentTime and getHashValue ensures deterministic results.


63-77: Partial rollout acceptance test is properly implemented

This test correctly verifies that a release is allowed when the hash value (50) is equal to the rollout percentage (50%) at the halfway point of the rollout period.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants