Skip to content

Add Profile Timer Manager - #35

Open
bharathappali wants to merge 2 commits into
kruize:mvp_demofrom
bharathappali:feat-bulk-prof-5
Open

Add Profile Timer Manager#35
bharathappali wants to merge 2 commits into
kruize:mvp_demofrom
bharathappali:feat-bulk-prof-5

Conversation

@bharathappali

@bharathappali bharathappali commented Jun 25, 2026

Copy link
Copy Markdown
Member

This PR is built on top of #34

#34 needs to be merged before this PR

This PR adds the Profile Timer Manager

Summary by Sourcery

Manage recurring bulk profile jobs with configuration-specific scheduling and job tracking.

New Features:

  • Add recurring timers for enabled bulk profile configurations, including startup initialization, webhook-driven updates, cancellation, and graceful shutdown.
  • Track the number of jobs triggered for each configuration in addition to the global job count.

Enhancements:

  • Execute scheduled configuration jobs through the bulk experiment API and record successful job triggers.

@sourcery-ai

sourcery-ai Bot commented Jun 25, 2026

Copy link
Copy Markdown

Reviewer's Guide

Introduce a Profile Timer Manager and supporting bulk profile infrastructure to periodically trigger bulk jobs per profile, including REST client endpoints, data models, and job accounting.

Sequence diagram for scheduled bulk profile job execution

sequenceDiagram
    participant ProfileTimerManager
    participant BulkProfileService
    participant KruizeClient
    participant JobsService

    ProfileTimerManager->>BulkProfileService: getEnabledProfiles()
    BulkProfileService-->>ProfileTimerManager: List<BulkProfile>

    loop for each BulkProfile
        ProfileTimerManager->>BulkProfileService: parseScheduling(scheduling)
        BulkProfileService-->>ProfileTimerManager: Duration
        ProfileTimerManager->>ProfileTimerManager: scheduleProfile(profile)
    end

    ProfileTimerManager->>ProfileTimerManager: executeProfileJob(profile)
    ProfileTimerManager->>BulkProfileService: convertProfileToBulkJob(profile)
    BulkProfileService-->>ProfileTimerManager: Map bulkJob
    ProfileTimerManager->>KruizeClient: bulkCreateExperiments(bulkJob)
    KruizeClient-->>ProfileTimerManager: String response
    ProfileTimerManager->>JobsService: incrementJobsTriggered(profileName)
Loading

File-Level Changes

Change Details Files
Track jobs triggered per profile name and log per-profile counts.
  • Add a ConcurrentHashMap to store job counts keyed by profile name.
  • Introduce an overloaded incrementJobsTriggered method that accepts a profile name and updates global and per-profile counters.
  • Enhance logging to include profile-specific job counts when triggering jobs.
src/main/java/com/kruize/optimizer/service/JobsService.java
Expose bulk profile retrieval endpoints in the Kruize REST client and add related constants.
  • Add BULK_PROFILES_ENDPOINT and PROFILE_NAME constants for bulk profile APIs.
  • Declare getBulkProfiles and getBulkProfile methods on the KruizeClient interface using the new endpoint and query parameter.
  • Ensure both new client methods produce JSON responses via JAX-RS annotations.
src/main/java/com/kruize/optimizer/client/KruizeClient.java
src/main/java/com/kruize/optimizer/utils/OptimizerConstants.java
Add ProfileTimerManager to schedule and manage recurring bulk jobs per profile.
  • Create an application-scoped ProfileTimerManager that uses a ScheduledExecutorService to schedule per-profile tasks.
  • Implement scheduleProfile to parse scheduling metadata and register a fixed-rate job execution task, replacing any existing timer for that profile.
  • Implement updateProfileTimer to react to webhook-driven profile updates, cancel/replace timers based on enabled flag and new interval.
  • Implement executeProfileJob to convert a BulkProfile into a bulk job, call the bulkCreateExperiments API, and record the job against JobsService.
  • Add lifecycle methods initializeProfiles to bootstrap timers for enabled profiles and shutdown to cleanly cancel timers and stop the executor.
  • Expose getActiveTimerCount to support observability or testing of active timers.
src/main/java/com/kruize/optimizer/service/ProfileTimerManager.java
Introduce BulkProfileService to fetch bulk profiles from Kruize and map them to bulk job requests.
  • Implement getEnabledProfiles to call KruizeClient.getBulkProfiles, deserialize the JSON into BulkProfile objects, and filter to enabled profiles.
  • Add parseScheduling to convert human-readable scheduling strings (e.g., 24h, 15min, 2d) into java.time.Duration using a regex-based parser.
  • Implement convertProfileToBulkJob to transform BulkProfile and its ClusterConfig into a Map representing the bulk job API payload (filters, datasource, metadata_profile, measurement_duration, webhook).
  • Wire BulkProfileService with RestClient-based KruizeClient and Jackson ObjectMapper via CDI.
  • Log key operations and error conditions for profile retrieval and conversion.
src/main/java/com/kruize/optimizer/service/BulkProfileService.java
Define data model classes for bulk profiles, cluster configuration, and recommendation settings used by profile-based bulk jobs.
  • Add BulkProfile model with JSON-mapped fields for profile metadata, clusters, recommendation settings, webhook URL, and enablement flags.
  • Add ClusterConfig model capturing cluster name, datasources, namespaces, labels, experiment types, and metadata profile with appropriate JSON properties.
  • Add RecommendationSettings model to represent scheduling cadence, terms, models, and measurement duration for recommendations.
  • Provide constructors, getters, and setters for all new model fields to support Jackson deserialization and internal usage.
src/main/java/com/kruize/optimizer/model/kruize/BulkProfile.java
src/main/java/com/kruize/optimizer/model/kruize/ClusterConfig.java
src/main/java/com/kruize/optimizer/model/kruize/RecommendationSettings.java

Possibly linked issues

  • #Implement Bulk Profile API Integration for Dynamic Scheduling: PR adds BulkProfile models, services, timers, and API calls implementing dynamic profile-based scheduling from the issue.

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've found 3 issues, and left some high level feedback:

  • ProfileTimerManager currently always schedules with an initial delay of 0; consider using the parsed scheduling interval (or a configurable value) for the initial delay to avoid immediate execution on startup or after updates if that’s not desired behavior.
  • BulkProfileService.convertProfileToBulkJob only uses the first ClusterConfig and silently ignores additional clusters; if multiple clusters are expected, you may want to either aggregate them or explicitly validate and fail fast to avoid surprising partial behavior.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- ProfileTimerManager currently always schedules with an initial delay of 0; consider using the parsed scheduling interval (or a configurable value) for the initial delay to avoid immediate execution on startup or after updates if that’s not desired behavior.
- BulkProfileService.convertProfileToBulkJob only uses the first ClusterConfig and silently ignores additional clusters; if multiple clusters are expected, you may want to either aggregate them or explicitly validate and fail fast to avoid surprising partial behavior.

## Individual Comments

### Comment 1
<location path="src/main/java/com/kruize/optimizer/service/ProfileTimerManager.java" line_range="67-68" />
<code_context>
+        cancelProfileTimer(profileName);
+
+        // Parse scheduling interval
+        Duration interval = bulkProfileService.parseScheduling(
+                profile.getRecommendationSettings().getScheduling()
+        );
+
</code_context>
<issue_to_address>
**issue (bug_risk):** Guard against null `recommendationSettings` when parsing scheduling.

If `profile.getRecommendationSettings()` can be null, this line will cause a `NullPointerException` and stop `scheduleProfile` / `initializeProfiles` from scheduling timers. Please add a null check and either use a safe default interval or fail fast with a clear error that includes the profile name.
</issue_to_address>

### Comment 2
<location path="src/main/java/com/kruize/optimizer/service/BulkProfileService.java" line_range="174-175" />
<code_context>
+        }
+
+        // Add measurement duration from recommendation settings
+        String measurementDuration = profile.getRecommendationSettings().getMeasurementDuration();
+        if (measurementDuration != null && !measurementDuration.isEmpty()) {
+            bulkJob.put("measurement_duration", measurementDuration);
+        }
</code_context>
<issue_to_address>
**issue:** Handle possible null `recommendationSettings` when building the bulk job.

`profile.getRecommendationSettings()` can be null, leading to a `NullPointerException` when calling `getMeasurementDuration()`. Add a null check or validate the presence of `recommendationSettings` before this call, and define the behavior when it’s missing (e.g., omit `measurement_duration` or use a default).
</issue_to_address>

### Comment 3
<location path="src/main/java/com/kruize/optimizer/service/BulkProfileService.java" line_range="93-95" />
<code_context>
+            throw new IllegalArgumentException("Scheduling string cannot be null or empty");
+        }
+
+        Matcher matcher = SCHEDULING_PATTERN.matcher(scheduling.trim().toLowerCase());
+        if (!matcher.matches()) {
+            throw new IllegalArgumentException("Invalid scheduling format: " + scheduling);
+        }
+
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Invalid scheduling formats currently cause an unchecked exception that may disrupt timer initialization.

Because `scheduleProfile` and `initializeProfiles` both call this logic, a single malformed `scheduling` string can prevent all timers from being initialized. Consider handling the error locally (e.g., log and skip the offending profile or fall back to a safe default interval) instead of letting the `IllegalArgumentException` propagate.

Suggested implementation:

```java
    public Duration parseScheduling(String scheduling) {
        // Use a safe default so that a single malformed profile does not block timer initialization
        Duration defaultDuration = Duration.ofMinutes(5);

        if (scheduling == null || scheduling.trim().isEmpty()) {
            LOGGER.warn("Scheduling string is null or empty. Falling back to default interval: " + defaultDuration);
            return defaultDuration;
        }

```

```java
        Matcher matcher = SCHEDULING_PATTERN.matcher(scheduling.trim().toLowerCase());
        if (!matcher.matches()) {
            LOGGER.warn("Invalid scheduling format '" + scheduling + "'. Falling back to default interval: " + defaultDuration);
            return defaultDuration;
        }

```

1. Ensure `BulkProfileService` has a logger instance, e.g. `private static final Logger LOGGER = Logger.getLogger(BulkProfileService.class);`.  
2. If other parts of the code rely on `IllegalArgumentException` from `parseScheduling` (e.g., tests or callers doing explicit error handling), update them to work with the new behavior (using the returned default duration instead of catching exceptions).  
3. Optionally, extract `Duration.ofMinutes(5)` into a named constant (e.g., `private static final Duration DEFAULT_SCHEDULING_INTERVAL = Duration.ofMinutes(5);`) and reuse it across the class, including `scheduleProfile` / `initializeProfiles` if they need the same fallback logic.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +67 to +68
Duration interval = bulkProfileService.parseScheduling(
profile.getRecommendationSettings().getScheduling()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (bug_risk): Guard against null recommendationSettings when parsing scheduling.

If profile.getRecommendationSettings() can be null, this line will cause a NullPointerException and stop scheduleProfile / initializeProfiles from scheduling timers. Please add a null check and either use a safe default interval or fail fast with a clear error that includes the profile name.

Comment thread src/main/java/com/kruize/optimizer/service/BulkProfileService.java Outdated
Comment on lines +93 to +95
Matcher matcher = SCHEDULING_PATTERN.matcher(scheduling.trim().toLowerCase());
if (!matcher.matches()) {
throw new IllegalArgumentException("Invalid scheduling format: " + scheduling);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion (bug_risk): Invalid scheduling formats currently cause an unchecked exception that may disrupt timer initialization.

Because scheduleProfile and initializeProfiles both call this logic, a single malformed scheduling string can prevent all timers from being initialized. Consider handling the error locally (e.g., log and skip the offending profile or fall back to a safe default interval) instead of letting the IllegalArgumentException propagate.

Suggested implementation:

    public Duration parseScheduling(String scheduling) {
        // Use a safe default so that a single malformed profile does not block timer initialization
        Duration defaultDuration = Duration.ofMinutes(5);

        if (scheduling == null || scheduling.trim().isEmpty()) {
            LOGGER.warn("Scheduling string is null or empty. Falling back to default interval: " + defaultDuration);
            return defaultDuration;
        }
        Matcher matcher = SCHEDULING_PATTERN.matcher(scheduling.trim().toLowerCase());
        if (!matcher.matches()) {
            LOGGER.warn("Invalid scheduling format '" + scheduling + "'. Falling back to default interval: " + defaultDuration);
            return defaultDuration;
        }
  1. Ensure BulkProfileService has a logger instance, e.g. private static final Logger LOGGER = Logger.getLogger(BulkProfileService.class);.
  2. If other parts of the code rely on IllegalArgumentException from parseScheduling (e.g., tests or callers doing explicit error handling), update them to work with the new behavior (using the returned default duration instead of catching exceptions).
  3. Optionally, extract Duration.ofMinutes(5) into a named constant (e.g., private static final Duration DEFAULT_SCHEDULING_INTERVAL = Duration.ofMinutes(5);) and reuse it across the class, including scheduleProfile / initializeProfiles if they need the same fallback logic.

Signed-off-by: bharathappali <abharath@redhat.com>
Signed-off-by: bharathappali <abharath@redhat.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: Under Review

Development

Successfully merging this pull request may close these issues.

1 participant