Skip to content

Layers -05 Add DAO and Converter support for List Layers functionality#1757

Merged
dinogun merged 12 commits intokruize:mvp_demofrom
bhanvimenghani:layer-dao-list-implementation
Feb 5, 2026
Merged

Layers -05 Add DAO and Converter support for List Layers functionality#1757
dinogun merged 12 commits intokruize:mvp_demofrom
bhanvimenghani:layer-dao-list-implementation

Conversation

@bhanvimenghani
Copy link
Contributor

@bhanvimenghani bhanvimenghani commented Jan 13, 2026

Description

Implements backend data layer for listing layers from database

Changes

  • DAO Layer: Added loadAllLayers() and loadLayerByName() methods with transaction handling and metrics
  • Converters: Added bidirectional conversion between KruizeLMLayerEntry (DB) ↔ KruizeLayer (domain)
  • Service Layer: Added loadAllLayers() to orchestrate DB access and conversion
  • Metrics: Configured Prometheus timers for performance monitoring

Files Modified

  • ExperimentDAO.java - Interface definitions
  • ExperimentDAOImpl.java - DAO implementation with Hibernate queries
  • DBHelpers.java - Data conversion logic (DB ↔ Domain)
  • ExperimentDBService.java - Service layer orchestration
  • DBConstants.java - HQL query constants
  • MetricsConfig.java - Performance metrics configuration

Fixes # (issue)

Type of change

  • Bug fix
  • New feature
  • Docs update
  • Breaking change (What changes might users need to make in their application due to this PR?)
  • Requires DB changes

How has this been tested?

Please describe the tests that were run to verify your changes and steps to reproduce. Please specify any test configuration required.

  • New Test X
  • Functional testsuite

Test Configuration

  • Kubernetes clusters tested on:

Checklist 🎯

  • Followed coding guidelines
  • Comments added
  • Dependent changes merged
  • Documentation updated
  • Tests added or updated

Additional information

Include any additional information such as links, test results, screenshots here

Summary by Sourcery

Add support for loading and converting layer entities between the database and domain model, with metrics instrumentation for the new DAO operations.

New Features:

  • Introduce DAO methods to load all layers and fetch layers by name from the database.
  • Provide converters to transform layer database entries into domain KruizeLayer objects, including metadata, presence, and tunables.
  • Expose a service-layer method to load all layers into an in-memory map keyed by layer name.

Enhancements:

  • Add a dedicated LayerConversionException with sectional context to improve error reporting during layer conversions.
  • Validate layer presence data source values against supported monitoring agents before assignment.
  • Extend database query constants and metrics configuration to cover the new layer loading operations.

@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Jan 13, 2026

Reviewer's Guide

Implements backend support to list layers from the database by adding DAO queries, DB↔domain converters, a service method to load all layers into a map, and associated Prometheus timing metrics and validations.

Sequence diagram for loadAllLayers service and DAO interaction

sequenceDiagram
    actor Caller
    participant ExperimentDBService
    participant ExperimentDAO as ExperimentDAO_ExperimentDAOImpl
    participant HibernateSessionFactory as KruizeHibernateUtil
    participant Session
    participant Database as DB
    participant DBHelpers as KruizeObjectConverters
    participant MetricsConfig

    Caller->>ExperimentDBService: loadAllLayers(layerMap)
    alt layerMap is null
        ExperimentDBService-->>Caller: return
    else layerMap not null
        ExperimentDBService->>ExperimentDAO: loadAllLayers()
        activate ExperimentDAO
        ExperimentDAO->>MetricsConfig: meterRegistry()
        MetricsConfig-->>ExperimentDAO: MeterRegistry
        ExperimentDAO->>MetricsConfig: start Timer.Sample for loadAllLayers
        ExperimentDAO->>HibernateSessionFactory: getSessionFactory().openSession()
        HibernateSessionFactory-->>ExperimentDAO: Session
        activate Session
        ExperimentDAO->>Session: createQuery(SELECT_FROM_LAYER)
        Session->>DB: execute HQL query
        DB-->>Session: List<KruizeLMLayerEntry>
        Session-->>ExperimentDAO: entries
        deactivate Session
        ExperimentDAO->>MetricsConfig: register timerLoadAllLayers with status=success
        ExperimentDAO->>MetricsConfig: stop Timer.Sample
        ExperimentDAO-->>ExperimentDBService: List<KruizeLMLayerEntry>
        deactivate ExperimentDAO

        alt entries not null and not empty
            ExperimentDBService->>DBHelpers: convertLayerEntryToLayerObject(entries)
            activate DBHelpers
            loop for each KruizeLMLayerEntry
                DBHelpers->>DBHelpers: convertLayerDBObjToLayerObject(entry)
                alt conversion fails
                    DBHelpers-->>ExperimentDBService: LayerConversionException
                    ExperimentDBService-->>Caller: propagate exception
                else conversion ok
                    DBHelpers-->>ExperimentDBService: KruizeLayer
                end
            end
            deactivate DBHelpers
            ExperimentDBService->>ExperimentDBService: populate layerMap with layer.getLayerName()
        end
        ExperimentDBService-->>Caller: return
    end
Loading

Sequence diagram for LayerPresenceQuery dataSource validation

sequenceDiagram
    actor Caller
    participant LayerPresenceQuery
    participant KruizeSupportedTypes

    Caller->>LayerPresenceQuery: setDataSource(dataSource)
    alt dataSource is not null and in MONITORING_AGENTS_SUPPORTED
        LayerPresenceQuery->>KruizeSupportedTypes: MONITORING_AGENTS_SUPPORTED.contains(dataSource.toLowerCase())
        KruizeSupportedTypes-->>LayerPresenceQuery: true
        LayerPresenceQuery->>LayerPresenceQuery: this.dataSource = dataSource
    else dataSource invalid or null
        LayerPresenceQuery->>KruizeSupportedTypes: MONITORING_AGENTS_SUPPORTED.contains(dataSource.toLowerCase())
        KruizeSupportedTypes-->>LayerPresenceQuery: false
        LayerPresenceQuery->>LayerPresenceQuery: do not modify dataSource
    end
    LayerPresenceQuery-->>Caller: return
Loading

Class diagram for new layer loading and conversion flow

classDiagram
    class ExperimentDBService {
        +void loadAllLayers(Map~String,KruizeLayer~ layerMap) throws Exception
        +ValidationOutputData addLayerToDB(KruizeLayer kruizeLayer)
        -ExperimentDAO experimentDAO
    }

    class ExperimentDAO {
        <<interface>>
        +ValidationOutputData addLayerToDB(KruizeLMLayerEntry kruizeLayerEntry)
        +List~KruizeLMLayerEntry~ loadAllLayers() throws Exception
        +List~KruizeLMLayerEntry~ loadLayerByName(String layerName) throws Exception
    }

    class ExperimentDAOImpl {
        +List~KruizeLMLayerEntry~ loadAllLayers() throws Exception
        +List~KruizeLMLayerEntry~ loadLayerByName(String layerName) throws Exception
        +List~KruizeLMMetadataProfileEntry~ loadAllMetadataProfiles() throws Exception
        -Logger LOGGER
    }

    class DBHelpers {
    }

    class Converters {
    }

    class KruizeObjectConverters {
        +KruizeLMLayerEntry convertLayerObjectToLayerDBObj(KruizeLayer kruizeLayer) throws Exception
        +KruizeLayer convertLayerDBObjToLayerObject(KruizeLMLayerEntry entry) throws LayerConversionException
        +List~KruizeLayer~ convertLayerEntryToLayerObject(List~KruizeLMLayerEntry~ kruizeLayerEntryList) throws LayerConversionException
    }

    class KruizeLMLayerEntry {
        +String getApi_version()
        +String getKind()
        +String getLayer_name()
        +Integer getLayer_level()
        +Object getDetails()
        +Object getMetadata()
        +Object getLayer_presence()
        +Object getTunables()
    }

    class KruizeLayer {
        +void setApiVersion(String apiVersion)
        +void setKind(String kind)
        +void setLayerName(String layerName)
        +void setLayerLevel(Integer layerLevel)
        +void setDetails(Object details)
        +void setMetadata(LayerMetadata metadata)
        +void setLayerPresence(LayerPresence layerPresence)
        +void setTunables(List~Tunable~ tunables)
        +String getLayerName()
    }

    class LayerMetadata {
    }

    class LayerPresence {
    }

    class Tunable {
    }

    class LayerConversionException {
        +LayerConversionException(AnalyzerConstants.LayerConversionSection section, String message, Throwable cause)
        +AnalyzerConstants.LayerConversionSection getSection()
        -AnalyzerConstants.LayerConversionSection section
    }

    class AnalyzerConstants {
    }

    class LayerConversionSection {
        <<enum>>
        BASIC_FIELDS
        METADATA
        LAYER_PRESENCE
        TUNABLES
    }

    class MetricsConfig {
        +static Timer timerAddLayerDB
        +static Timer timerLoadAllLayers
        +static Timer timerLoadLayerByName
        +static Timer.Builder timerBAddLayerDB
        +static Timer.Builder timerBLoadAllLayers
        +static Timer.Builder timerBLoadLayerByName
        +static MeterRegistry meterRegistry()
    }

    class LayerPresenceQuery {
        -String dataSource
        +String getDataSource()
        +void setDataSource(String dataSource)
    }

    class KruizeSupportedTypes {
        +static Set~String~ MONITORING_AGENTS_SUPPORTED
    }

    ExperimentDBService --> ExperimentDAO : uses
    ExperimentDAOImpl ..|> ExperimentDAO
    ExperimentDBService --> KruizeObjectConverters : uses
    ExperimentDAOImpl --> KruizeLMLayerEntry : queries
    ExperimentDAOImpl --> DBConstants : uses HQL constants
    ExperimentDAOImpl --> MetricsConfig : records timers
    KruizeObjectConverters --> KruizeLMLayerEntry : converts from
    KruizeObjectConverters --> KruizeLayer : converts to
    KruizeObjectConverters --> LayerConversionException : throws
    KruizeObjectConverters --> LayerMetadata
    KruizeObjectConverters --> LayerPresence
    KruizeObjectConverters --> Tunable
    LayerConversionException --> AnalyzerConstants : uses
    AnalyzerConstants --> LayerConversionSection
    LayerPresenceQuery --> KruizeSupportedTypes : validates dataSource

    DBHelpers *-- Converters
    Converters *-- KruizeObjectConverters
Loading

File-Level Changes

Change Details Files
Add DB-to-domain conversion utilities for layer entities, including collection handling and structured error reporting.
  • Add convertLayerDBObjToLayerObject to map KruizeLMLayerEntry fields and JSON blobs into a KruizeLayer domain object, with sectioned error handling via LayerConversionException.
  • Add convertLayerEntryToLayerObject to convert lists of KruizeLMLayerEntry to lists of KruizeLayer, failing the whole operation on any individual conversion error.
  • Simplify convertLayerObjectToLayerDBObj signature to use the imported KruizeLayer type directly.
src/main/java/com/autotune/database/helper/DBHelpers.java
src/main/java/com/autotune/analyzer/exceptions/LayerConversionException.java
src/main/java/com/autotune/analyzer/utils/AnalyzerConstants.java
Introduce DAO methods and queries to load layer records from the database with Prometheus timing metrics.
  • Extend ExperimentDAO with loadAllLayers and loadLayerByName signatures.
  • Implement loadAllLayers and loadLayerByName in ExperimentDAOImpl using Hibernate HQL queries with error logging and wrapped exceptions.
  • Add HQL constants for selecting all layers and selecting by layer name in DBConstants.
  • Add dedicated timers and timer builders for layer load operations to MetricsConfig and wire them into DAO methods.
src/main/java/com/autotune/database/dao/ExperimentDAO.java
src/main/java/com/autotune/database/dao/ExperimentDAOImpl.java
src/main/java/com/autotune/database/helper/DBConstants.java
src/main/java/com/autotune/utils/MetricsConfig.java
Expose a service-layer method to load and cache all layers in memory keyed by layer name.
  • Add ExperimentDBService.loadAllLayers to fetch all KruizeLMLayerEntry records via the DAO, convert them to KruizeLayer objects, and populate a provided Map keyed by layerName.
  • Guard against null maps and empty result sets to avoid unnecessary work.
src/main/java/com/autotune/database/service/ExperimentDBService.java
Tighten validation on layer presence data source selection.
  • Restrict LayerPresenceQuery.setDataSource to only accept values present in KruizeSupportedTypes.MONITORING_AGENTS_SUPPORTED, ignoring unsupported inputs.
src/main/java/com/autotune/analyzer/kruizeLayer/LayerPresenceQuery.java

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

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

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

  • The layer conversion helpers in DBHelpers mix ObjectMapper and Gson, throw/catch generic Exceptions, and call printStackTrace; consider standardizing on a single JSON library and surfacing more specific checked/unchecked exceptions instead of swallowing details at this low level.
  • The DAO methods for layers (addLayerToDB/loadAllLayers/loadLayerByName) re-register timers on every call using the builder; this can lead to metric registration churn and tag-cardinality issues—prefer reusing pre-registered Timer instances and only tagging via Sample where needed.
  • ExperimentDBService.addLayerToDB logs exceptions but always returns a ValidationOutputData object whose message is often null; consider setting an explicit error message or propagating the exception so callers can distinguish failure reasons more reliably.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The layer conversion helpers in DBHelpers mix ObjectMapper and Gson, throw/catch generic Exceptions, and call printStackTrace; consider standardizing on a single JSON library and surfacing more specific checked/unchecked exceptions instead of swallowing details at this low level.
- The DAO methods for layers (addLayerToDB/loadAllLayers/loadLayerByName) re-register timers on every call using the builder; this can lead to metric registration churn and tag-cardinality issues—prefer reusing pre-registered Timer instances and only tagging via Sample where needed.
- ExperimentDBService.addLayerToDB logs exceptions but always returns a ValidationOutputData object whose message is often null; consider setting an explicit error message or propagating the exception so callers can distinguish failure reasons more reliably.

## Individual Comments

### Comment 1
<location> `src/main/java/com/autotune/database/service/ExperimentDBService.java:473-474` </location>
<code_context>
+        Timer.Sample timerAddLayerDB = Timer.start(MetricsConfig.meterRegistry());
+        Transaction tx = null;
+        try (Session session = KruizeHibernateUtil.getSessionFactory().openSession()) {
+            try {
+                tx = session.beginTransaction();
+                session.persist(kruizeLayerEntry);
</code_context>

<issue_to_address>
**issue (bug_risk):** Handle possible null return from convertLayerObjectToLayerDBObj before calling DAO

convertLayerObjectToLayerDBObj may return null on conversion failure, but its result is used here without a check. If null is passed to experimentDAO.addLayerToDB, this could trigger a runtime exception (e.g., from Hibernate). Add a null guard and, on failure, return a failed ValidationOutputData with a clear error message instead of calling the DAO.
</issue_to_address>

### Comment 2
<location> `src/main/java/com/autotune/database/helper/DBHelpers.java:1611-1612` </location>
<code_context>
+                validationOutputData.setSuccess(false);
+                validationOutputData.setMessage(e.getMessage());
+            }
+        } catch (Exception e) {
+            LOGGER.error("Not able to save layer due to {}", e.getMessage());
+            validationOutputData.setMessage(e.getMessage());
</code_context>

<issue_to_address>
**suggestion (bug_risk):** Avoid swallowing conversion exceptions and only logging the message

By catching `Exception` and only logging `e.getMessage()`, callers lose the original exception type, stack trace, and context, and the method may proceed with invalid or partially initialized data. Prefer either letting the specific exception propagate or wrapping it in a domain-specific exception while preserving the cause, and log with `LOGGER.error(..., e)` instead of using `printStackTrace()` or message-only logging.
</issue_to_address>

### Comment 3
<location> `src/main/java/com/autotune/database/helper/DBHelpers.java:1683-1692` </location>
<code_context>
+            public static List<KruizeLayer> convertLayerEntryToLayerObject(List<KruizeLMLayerEntry> kruizeLayerEntryList) throws Exception {
</code_context>

<issue_to_address>
**suggestion (bug_risk):** Preserve original exception cause instead of throwing a new generic Exception

In the outer catch, the original exception is logged but then replaced with `new Exception(e.getMessage())`, which discards its type and stack trace. Instead, either rethrow the original exception or wrap it with `throw new Exception("...", e);` so callers retain access to the root cause.

Suggested implementation:

```java
            throw new Exception("Error while converting layer entries to layer objects", e);

```

If you prefer to keep the original exception type entirely, you can instead replace the line with `throw e;` so callers get the exact original exception without wrapping.
</issue_to_address>

### Comment 4
<location> `src/main/java/com/autotune/analyzer/kruizeLayer/presence/LayerPresenceQuery.java:39-48` </location>
<code_context>
+	public LayerPresenceQuery() {
+	}
+
+	public LayerPresenceQuery(String datasource,
+							  String layerPresenceQuery,
+							  String layerPresenceKey) throws MonitoringAgentNotSupportedException {
+
+		if (KruizeSupportedTypes.MONITORING_AGENTS_SUPPORTED.contains(datasource)) {
+			this.dataSource = datasource;
+		} else {
+			throw new MonitoringAgentNotSupportedException(
+				"Unsupported monitoring agent datasource: '" + datasource + "'. " +
+				"Supported datasources: " + KruizeSupportedTypes.MONITORING_AGENTS_SUPPORTED
+			);
+		}
+		this.layerPresenceQuery = layerPresenceQuery;
+		this.layerPresenceKey = layerPresenceKey;
+	}
+
</code_context>

<issue_to_address>
**issue (bug_risk):** Constructor validates datasource, but setters allow bypassing that validation

The arg-based constructor validates `datasource` against `KruizeSupportedTypes.MONITORING_AGENTS_SUPPORTED`, but other code paths (e.g., default constructor + setter / deserialization) can assign an unchecked value. To preserve the constraint that only supported monitoring agents are used, either apply the same validation in the setter or make `dataSource` immutable and settable only via the validating constructor.
</issue_to_address>

### Comment 5
<location> `src/main/java/com/autotune/analyzer/kruizeLayer/LayerPresence.java:46-55` </location>
<code_context>
+     * @return LayerPresenceDetector implementation
+     */
+    public LayerPresenceDetector getDetector() {
+        // Check for query-based presence
+        if (queries != null && !queries.isEmpty()) {
+            return new QueryBasedPresence(queries);
+        }
+
+        // Check for label-based presence
+        if (label != null && !label.isEmpty()) {
+            return new LabelBasedPresence(label);
+        }
+
+        // Default to presence always
+        if (presence != null) {
+            return new PresenceAlways(presence);
+        }
+
+        // If nothing is set, default to "always"
+        return new PresenceAlways("always");
+    }
</code_context>

<issue_to_address>
**suggestion:** Clarify or enforce precedence rules between presence, queries, and labels

This method prioritizes queries over labels and only falls back to `presence` (or "always") when both are unset. If a config sets queries together with labels or presence, the extra settings are silently ignored, which may confuse users. Consider either enforcing that only one mechanism is configured, or at least logging a warning when multiple are set but only one is used.

Suggested implementation:

```java
    /**
     * Factory method to get the appropriate LayerPresenceDetector implementation
     * based on what fields are populated.
     * <p>
     * Precedence rules when multiple mechanisms are configured:
     * queries &gt; label &gt; presence
     *
     * @return LayerPresenceDetector implementation
     */
    public LayerPresenceDetector getDetector() {
        boolean hasQueries = (queries != null && !queries.isEmpty());
        boolean hasLabel = (label != null && !label.isEmpty());
        boolean hasPresence = (presence != null);

        int configuredMechanisms = 0;
        if (hasQueries) {
            configuredMechanisms++;
        }
        if (hasLabel) {
            configuredMechanisms++;
        }
        if (hasPresence) {
            configuredMechanisms++;
        }

        if (configuredMechanisms > 1) {
            // Multiple presence mechanisms configured; warn and rely on documented precedence.
            LOGGER.warn(
                    "Multiple presence mechanisms configured for LayerPresence (queries: {}, label: {}, presence: {}). " +
                    "Using precedence: queries > label > presence.",
                    hasQueries, hasLabel, hasPresence
            );
        }

        // Check for query-based presence (highest precedence)
        if (hasQueries) {
            return new QueryBasedPresence(queries);
        }

        // Check for label-based presence
        if (hasLabel) {
            return new LabelBasedPresence(label);
        }

        // Default to presence always
        if (hasPresence) {
            return new PresenceAlways(presence);
        }

        // If nothing is set, default to "always"
        return new PresenceAlways("always");

```

1. Ensure a logger is available in this class. If it is not already defined, add near the top of `LayerPresence`:
   - Import:
     `import org.slf4j.Logger;`
     `import org.slf4j.LoggerFactory;`
   - Field:
     `private static final Logger LOGGER = LoggerFactory.getLogger(LayerPresence.class);`
2. If the project uses a different logging convention (e.g., `Log`, `Logger LOG`, or a custom logging wrapper), adjust the `LOGGER.warn(...)` usage to match the existing pattern.
3. If there are unit tests around `LayerPresence.getDetector()`, consider adding tests that:
   - Configure only `queries`, only `label`, only `presence`, and combinations thereof.
   - Assert that the correct detector type is chosen in each case.
   - Optionally verify that a warning is logged when multiple mechanisms are configured.
</issue_to_address>

### Comment 6
<location> `src/main/java/com/autotune/analyzer/utils/AnalyzerConstants.java:645-635` </location>
<code_context>
+        /**
+         * Enum for different types of layer presence detection
+         */
+        public enum PresenceType {
+            ALWAYS,
+            QUERY,
+            LABEL
+        }
+
+        public static final String DEFAULT_PRESENCE = "always";
</code_context>

<issue_to_address>
**suggestion:** Avoid duplicating PresenceType enum that already exists in LayerPresenceDetector

This enum duplicates `LayerPresenceDetector.PresenceType`, which risks them diverging over time. Prefer reusing `LayerPresenceDetector.PresenceType`, or refactor so there is a single source of truth that the other references or derives from.

Suggested implementation:

```java
    /**
     * Contains constants related to KruizeLayer presence detection
     */
    public static final class LayerConstants {
        // Default layer presence type, derived from LayerPresenceDetector.PresenceType
        public static final String DEFAULT_PRESENCE =
                LayerPresenceDetector.PresenceType.ALWAYS.name().toLowerCase();

        private LayerConstants() {
        }
    }

```

1. Add an import for `LayerPresenceDetector` at the top of `AnalyzerConstants.java`, using its correct package (for example: `import com.autotune.analyzer.layer.LayerPresenceDetector;`), matching the actual location in your codebase.
2. If your codebase standardizes lowercase constants via a specific utility or uses `Locale.ROOT` with `toLowerCase`, you may want to adjust `toLowerCase()` accordingly:
   `LayerPresenceDetector.PresenceType.ALWAYS.name().toLowerCase(Locale.ROOT);` and import `java.util.Locale`.
3. Update any usages of `AnalyzerConstants.LayerConstants.PresenceType` (if any were added elsewhere in the PR) to use `LayerPresenceDetector.PresenceType` directly.
</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.

@bhanvimenghani bhanvimenghani moved this to Under Review in Monitoring Jan 13, 2026
@bhanvimenghani bhanvimenghani force-pushed the layer-dao-list-implementation branch 2 times, most recently from ccc24a1 to 2b88bb8 Compare January 28, 2026 06:28
@dinogun
Copy link
Contributor

dinogun commented Jan 30, 2026

@bhanvimenghani Pls rebase to resolve conflicts

@bharathappali bharathappali force-pushed the layer-dao-list-implementation branch from 2b88bb8 to af54139 Compare January 30, 2026 10:42
@bharathappali
Copy link
Member

@dinogun I have rebased on behalf of @bhanvimenghani

Signed-off-by: bharathappali <abharath@redhat.com>
Signed-off-by: bharathappali <abharath@redhat.com>
Signed-off-by: bharathappali <abharath@redhat.com>
Signed-off-by: bharathappali <abharath@redhat.com>
@bharathappali bharathappali self-requested a review February 3, 2026 07:29
Copy link
Member

@bharathappali bharathappali left a comment

Choose a reason for hiding this comment

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

LGTM

@bharathappali bharathappali requested a review from dinogun February 3, 2026 07:29
@bharathappali
Copy link
Member

@sourcery-ai review

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

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

  • In LayerPresenceQuery.setDataSource, calling dataSource.toLowerCase() without a null check can throw an NPE and silently ignoring unsupported values (leaving the previous value unchanged) may be surprising to callers; consider validating/null-checking and failing fast or logging when an unsupported value is provided.
  • The new DB timer usage in ExperimentDAOImpl.loadAllLayers and loadLayerByName re-registers the timers on every call and overwrites the static timer fields; it would be cleaner to register the timers once in MetricsConfig and just start/stop them in the DAO methods without reassigning the static variables.
  • In ExperimentDAOImpl and the new converters, you wrap and rethrow exceptions with only the message, and in convertLayerDBObjToLayerObject you log the message and call printStackTrace() while swallowing failures and returning null; consider consistently propagating the original cause (new Exception(msg, e)) and avoiding printStackTrace()/silent nulls so callers can handle conversion/DB errors more predictably.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `LayerPresenceQuery.setDataSource`, calling `dataSource.toLowerCase()` without a null check can throw an NPE and silently ignoring unsupported values (leaving the previous value unchanged) may be surprising to callers; consider validating/null-checking and failing fast or logging when an unsupported value is provided.
- The new DB timer usage in `ExperimentDAOImpl.loadAllLayers` and `loadLayerByName` re-registers the timers on every call and overwrites the static timer fields; it would be cleaner to register the timers once in `MetricsConfig` and just start/stop them in the DAO methods without reassigning the static variables.
- In `ExperimentDAOImpl` and the new converters, you wrap and rethrow exceptions with only the message, and in `convertLayerDBObjToLayerObject` you log the message and call `printStackTrace()` while swallowing failures and returning null; consider consistently propagating the original cause (`new Exception(msg, e)`) and avoiding `printStackTrace()`/silent nulls so callers can handle conversion/DB errors more predictably.

## Individual Comments

### Comment 1
<location> `src/main/java/com/autotune/analyzer/kruizeLayer/LayerPresenceQuery.java:60-61` </location>
<code_context>

 	public void setDataSource(String dataSource) {
-		this.dataSource = dataSource;
+		if (KruizeSupportedTypes.MONITORING_AGENTS_SUPPORTED.contains(dataSource.toLowerCase()))
+			this.dataSource = dataSource;
 	}

</code_context>

<issue_to_address>
**issue (bug_risk):** Guard against null and consider behavior when dataSource is unsupported

`dataSource.toLowerCase()` will throw an NPE if `dataSource` is null. Also, unsupported values are silently ignored, leaving the previous `this.dataSource` and making behavior harder to trace. Consider adding a null check and explicitly handling unsupported values (e.g., early return with a log or exception).
</issue_to_address>

### Comment 2
<location> `src/main/java/com/autotune/database/helper/DBHelpers.java:1619-1628` </location>
<code_context>
+            public static KruizeLayer convertLayerDBObjToLayerObject(KruizeLMLayerEntry entry) {
</code_context>

<issue_to_address>
**issue (bug_risk):** Exception handling here both swallows the error and uses e.printStackTrace

This method logs and returns `null` on any exception, so callers can’t distinguish a real `null` from a conversion failure. It also uses `e.printStackTrace()`, which isn’t appropriate for production. Prefer propagating the exception or using a result type (e.g., `Optional<KruizeLayer>`) that makes failures explicit, and rely on structured logging instead of `printStackTrace()`.
</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.

Signed-off-by: bharathappali <abharath@redhat.com>
Signed-off-by: bharathappali <abharath@redhat.com>
Signed-off-by: bharathappali <abharath@redhat.com>
@bharathappali
Copy link
Member

@sourcery-ai review

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

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:

  • In the layer conversion helpers, convertLayerEntryToLayerObject currently declares and throws a generic Exception and then wraps LayerConversionException into a plain Exception, which discards the structured section information and stack trace; consider narrowing the method signature to LayerConversionException (or including it) and rethrowing the original cause to preserve context.
  • In the new DAO methods (loadAllLayers, loadLayerByName), you catch a generic Exception and wrap it in a new Exception with only the message, which loses the original cause and stack trace; it would be better to chain the cause (e.g. throw new Exception("...", e)) or introduce a dedicated data-access exception type.
  • The metrics wiring in loadAllLayers and loadLayerByName re-registers Timer instances on every call via timerBLoadAllLayers.tag(...).register(...), which can create many time series and be expensive; consider registering the timers once (e.g. statically or during initialization) and only tagging/stopping samples per call.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In the layer conversion helpers, `convertLayerEntryToLayerObject` currently declares and throws a generic `Exception` and then wraps `LayerConversionException` into a plain `Exception`, which discards the structured section information and stack trace; consider narrowing the method signature to `LayerConversionException` (or including it) and rethrowing the original cause to preserve context.
- In the new DAO methods (`loadAllLayers`, `loadLayerByName`), you catch a generic `Exception` and wrap it in a new `Exception` with only the message, which loses the original cause and stack trace; it would be better to chain the cause (e.g. `throw new Exception("...", e)`) or introduce a dedicated data-access exception type.
- The metrics wiring in `loadAllLayers` and `loadLayerByName` re-registers `Timer` instances on every call via `timerBLoadAllLayers.tag(...).register(...)`, which can create many time series and be expensive; consider registering the timers once (e.g. statically or during initialization) and only tagging/stopping samples per call.

## Individual Comments

### Comment 1
<location> `src/main/java/com/autotune/database/helper/DBHelpers.java:1693-1702` </location>
<code_context>
+            public static List<KruizeLayer> convertLayerEntryToLayerObject(List<KruizeLMLayerEntry> kruizeLayerEntryList) throws Exception {
</code_context>

<issue_to_address>
**suggestion:** Preserve LayerConversionException details and avoid re-wrapping generic Exceptions in the conversion loop.

The method signature and handling are overly generic: it declares `throws Exception` and rewraps all errors (including `LayerConversionException`) into a new `Exception` while logging only `e.getMessage()`, which discards structured section data and the original stack trace. Narrow the catch to `LayerConversionException` (plus an optional catch for unexpected throwables), log the full exception, and either rethrow the original or wrap it as the cause (e.g., `new Exception("...", e)`). Update the method signature to `throws LayerConversionException` to better reflect the expected failure mode.

Suggested implementation:

```java
             * @param kruizeLayerEntryList List of KruizeLMLayerEntry database objects to be converted
             * @return List of KruizeLayer domain objects
             * @throws LayerConversionException if any KruizeLMLayerEntry cannot be converted to a KruizeLayer
             */
            public static List<KruizeLayer> convertLayerEntryToLayerObject(List<KruizeLMLayerEntry> kruizeLayerEntryList) throws LayerConversionException {

```

Because the snippet does not show the full method body, you’ll need to adjust the exception handling inside the conversion loop. Concretely:

1. Replace the generic catch with a typed catch for `LayerConversionException` and an optional fallback catch for unexpected exceptions. For example, inside the `for (KruizeLMLayerEntry entry : kruizeLayerEntryList) { ... }` loop:

   ```java
   try {
       if (null == entry)
           continue;

       KruizeLayer kruizeLayer = convertLayerDBObjToLayerObject(entry);
       kruizeLayerList.add(kruizeLayer);
   } catch (LayerConversionException e) {
       // Preserve full context and stack trace
       LOGGER.error("Layer conversion failed for entry: {}", entry, e);
       throw e;
   } catch (Exception e) {
       // Handle unexpected issues but preserve as cause
       LOGGER.error("Unexpected error during layer conversion for entry: {}", entry, e);
       throw new LayerConversionException("Unexpected error during layer conversion", e);
   }
   ```

2. If the file does not already import `LayerConversionException`, add at the top of `DBHelpers.java`:

   ```java
   import com.autotune.common.exceptions.LayerConversionException;
   ```

3. Ensure that any other callers of `convertLayerEntryToLayerObject(...)` are updated to handle `LayerConversionException` (either by declaring it in their own `throws` clause or by catching it).
</issue_to_address>

### Comment 2
<location> `src/main/java/com/autotune/database/dao/ExperimentDAOImpl.java:1452-1455` </location>
<code_context>
+        try (Session session = KruizeHibernateUtil.getSessionFactory().openSession()) {
+            entries = session.createQuery(DBConstants.SQLQUERY.SELECT_FROM_LAYER, KruizeLMLayerEntry.class).list();
+            statusValue = "success";
+        } catch (Exception e) {
+            LOGGER.error("Not able to load Layers due to {}", e.getMessage());
+            throw new Exception("Error while loading existing Layers from database due to : " + e.getMessage());
+        } finally {
+            if (null != timerLoadAllLayers) {
</code_context>

<issue_to_address>
**suggestion:** Retain original exception as cause and log full stack trace when loadAllLayers fails.

The catch block wraps the original `Exception` using only `e.getMessage()`, which loses the stack trace and concrete exception type, and logs only the message. Use `LOGGER.error("Not able to load Layers", e);` and preserve the cause when rethrowing, e.g. `throw new Exception("Error while loading existing Layers from database", e);` to keep full context for debugging.

```suggestion
        } catch (Exception e) {
            LOGGER.error("Not able to load Layers", e);
            throw new Exception("Error while loading existing Layers from database", e);
        } finally {
```
</issue_to_address>

### Comment 3
<location> `src/main/java/com/autotune/analyzer/kruizeLayer/LayerPresenceQuery.java:59-63` </location>
<code_context>

 	public void setDataSource(String dataSource) {
-		this.dataSource = dataSource;
+		if (null != dataSource &&
+				KruizeSupportedTypes.MONITORING_AGENTS_SUPPORTED.contains(dataSource.toLowerCase()))
+			this.dataSource = dataSource;
 	}
</code_context>

<issue_to_address>
**suggestion (bug_risk):** Setter silently ignores unsupported dataSource values, which may hide configuration errors.

Because the setter only updates `this.dataSource` when the value is non-null and in `MONITORING_AGENTS_SUPPORTED`, calls with unsupported values become silent no-ops and leave the previous value (or null) in place. This can mislead callers into believing the data source was changed. Consider either resetting `this.dataSource` to null for unsupported values, or throwing an `IllegalArgumentException` to fail fast on invalid input.

```suggestion
	public void setDataSource(String dataSource) {
		if (dataSource == null) {
			this.dataSource = null;
			return;
		}

		if (!KruizeSupportedTypes.MONITORING_AGENTS_SUPPORTED.contains(dataSource.toLowerCase())) {
			throw new IllegalArgumentException(
					"Unsupported dataSource: " + dataSource + ". Supported values are: "
							+ KruizeSupportedTypes.MONITORING_AGENTS_SUPPORTED);
		}

		this.dataSource = dataSource;
	}
```
</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.

Signed-off-by: bharathappali <abharath@redhat.com>
Signed-off-by: bharathappali <abharath@redhat.com>
@bharathappali
Copy link
Member

@sourcery-ai review

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

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:

  • The DAO method loadLayerByName returns a List<KruizeLMLayerEntry> even though it is logically expected to return at most a single record; consider tightening this to return an Optional<KruizeLMLayerEntry> (or throw if multiple found) so callers don't have to handle an unexpected collection.
  • In ExperimentDAOImpl.loadAllLayers and loadLayerByName, timers are re-registered on each call (MetricsConfig.timerBLoadAllLayers.tag(...).register(...)), which can create many distinct meters; it would be cleaner to register the timers once and only use Timer.Sample with Timer instances at call time.
  • In ExperimentDBService.loadAllLayers, the method silently returns when layerMap is null; consider either enforcing non-null via a precondition (e.g., Objects.requireNonNull) or documenting and avoiding this silent no-op to make incorrect usage fail fast.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The DAO method `loadLayerByName` returns a `List<KruizeLMLayerEntry>` even though it is logically expected to return at most a single record; consider tightening this to return an `Optional<KruizeLMLayerEntry>` (or throw if multiple found) so callers don't have to handle an unexpected collection.
- In `ExperimentDAOImpl.loadAllLayers` and `loadLayerByName`, timers are re-registered on each call (`MetricsConfig.timerBLoadAllLayers.tag(...).register(...)`), which can create many distinct meters; it would be cleaner to register the timers once and only use `Timer.Sample` with `Timer` instances at call time.
- In `ExperimentDBService.loadAllLayers`, the method silently returns when `layerMap` is null; consider either enforcing non-null via a precondition (e.g., `Objects.requireNonNull`) or documenting and avoiding this silent no-op to make incorrect usage fail fast.

## Individual Comments

### Comment 1
<location> `src/main/java/com/autotune/database/helper/DBHelpers.java:1622` </location>
<code_context>
+             */
+            public static KruizeLayer convertLayerDBObjToLayerObject(KruizeLMLayerEntry entry) throws LayerConversionException {
+                KruizeLayer kruizeLayer = new KruizeLayer();
+                Gson gson = new Gson();
+
+                try {
</code_context>

<issue_to_address>
**suggestion (performance):** Consider reusing a single Gson instance instead of creating a new one per conversion for efficiency.

`convertLayerDBObjToLayerObject` creates a new `Gson` each time, which is unnecessary overhead when loading many layers. Since `Gson` is thread-safe after configuration, consider a static/shared instance in this helper to reduce allocations and GC pressure in bulk operations (e.g. `loadAllLayers`).

Suggested implementation:

```java
            public static KruizeLayer convertLayerDBObjToLayerObject(KruizeLMLayerEntry entry) throws LayerConversionException {
                KruizeLayer kruizeLayer = new KruizeLayer();
                Gson gson = GSON;

```

To fully implement the optimization, also:
1. Add a shared `Gson` instance to the `DBHelpers` class, e.g.:
   - `private static final Gson GSON = new Gson();`
   Place this near other static fields in `DBHelpers`.
2. Ensure `com.google.gson.Gson` is imported at the top of the file if it is not already.
This will allow all conversions (including `convertLayerDBObjToLayerObject`) to reuse the same thread-safe `Gson` instance.
</issue_to_address>

### Comment 2
<location> `src/main/java/com/autotune/database/dao/ExperimentDAOImpl.java:1452-1461` </location>
<code_context>
+            entries = session.createQuery(DBConstants.SQLQUERY.SELECT_FROM_LAYER_BY_NAME, KruizeLMLayerEntry.class)
+                    .setParameter("layerName", layerName).list();
+            statusValue = "success";
+        } catch (Exception e) {
+            LOGGER.error("Not able to load Layer {} due to {}", layerName, e.getMessage());
+            throw new Exception("Error while loading Layer from database due to : " + e.getMessage());
+        } finally {
+            if (null != timerLoadLayerByName) {
</code_context>

<issue_to_address>
**issue:** Preserve the original exception as the cause when rethrowing to avoid losing stack trace and context.

In `loadLayerByName`, the rethrown `Exception` currently uses only `e.getMessage()` and drops the original cause, while `loadAllLayers` correctly chains the exception with `throw new Exception("Error while loading existing Layers from database", e);`. For consistency and better diagnosability, apply the same pattern here:

```java
throw new Exception("Error while loading Layer from database", e);
```
</issue_to_address>

### Comment 3
<location> `src/main/java/com/autotune/analyzer/kruizeLayer/LayerPresenceQuery.java:60-61` </location>
<code_context>

 	public void setDataSource(String dataSource) {
-		this.dataSource = dataSource;
+		if (null != dataSource &&
+				KruizeSupportedTypes.MONITORING_AGENTS_SUPPORTED.contains(dataSource.toLowerCase()))
+			this.dataSource = dataSource;
 	}
</code_context>

<issue_to_address>
**suggestion (bug_risk):** Silently ignoring unsupported dataSource values may make misconfigurations harder to detect.

The setter now only updates `this.dataSource` when the input is in `MONITORING_AGENTS_SUPPORTED`; otherwise it does nothing. This diverges from the previous behavior and can make invalid inputs appear to succeed. Please either fail fast (e.g., throw), return a status, or at least log unsupported values so configuration issues are detectable.

Suggested implementation:

```java
	public void setDataSource(String dataSource) {
		if (dataSource == null) {
			LOGGER.warn("LayerPresenceQuery: dataSource is null; resetting to null");
			this.dataSource = null;
			return;
		}

		String normalizedDataSource = dataSource.toLowerCase();
		if (KruizeSupportedTypes.MONITORING_AGENTS_SUPPORTED.contains(normalizedDataSource)) {
			this.dataSource = dataSource;
		} else {
			LOGGER.warn(
					"LayerPresenceQuery: Unsupported dataSource '{}'. Supported values are: {}",
					dataSource,
					KruizeSupportedTypes.MONITORING_AGENTS_SUPPORTED
			);
		}
	}

```

To make the above compile and log correctly, the following are also needed in `LayerPresenceQuery.java`:
1. Ensure a logger is defined in the class, e.g., using SLF4J:
   - At the top of the file (with other imports), add:
     `import org.slf4j.Logger;`
     `import org.slf4j.LoggerFactory;`
   - Inside the class, add:
     `private static final Logger LOGGER = LoggerFactory.getLogger(LayerPresenceQuery.class);`
2. If a different logging framework is used in this project (e.g., `java.util.logging` or a custom logger), adjust the logger type, factory method, and `warn` calls to match the existing conventions.
</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.

@bharathappali
Copy link
Member

@dinogun Please kindly review this PR

Signed-off-by: bharathappali <abharath@redhat.com>
Signed-off-by: bharathappali <abharath@redhat.com>
@bharathappali
Copy link
Member

@dinogun I have addressed your review comments and have also raised an issue for optimising the loadAll.. methods in ExperimentDAOImpl. I would like to request you to get this PR re-review. Thanks in advance!

Copy link
Contributor

@dinogun dinogun left a comment

Choose a reason for hiding this comment

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

LGTM

@dinogun dinogun merged commit 872a4a7 into kruize:mvp_demo Feb 5, 2026
3 checks passed
@github-project-automation github-project-automation bot moved this from Under Review to Done in Monitoring Feb 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

3 participants