Skip to content

BuildRetryPolicy for reliable membase#1315

Merged
Oceania2018 merged 2 commits intoSciSharp:masterfrom
hchen2020:master
Mar 26, 2026
Merged

BuildRetryPolicy for reliable membase#1315
Oceania2018 merged 2 commits intoSciSharp:masterfrom
hchen2020:master

Conversation

@hchen2020
Copy link
Copy Markdown
Contributor

No description provided.

@qodo-code-review
Copy link
Copy Markdown
Contributor

Review Summary by Qodo

Add retry policy with exponential backoff for Membase reliability

✨ Enhancement

Grey Divider

Walkthroughs

Description
• Implement retry policy with exponential backoff for Membase API calls
• Handle transient failures (HTTP 503, timeouts, task cancellations)
• Increase default timeout from 10 to 30 seconds for reliability
• Improve error logging with API exception details and serialized arguments
• Add Polly dependency for resilient HTTP communication patterns
Diagram
flowchart LR
  A["ExecuteQueryAsync"] --> B["BuildRetryPolicy"]
  B --> C["Timeout Policy"]
  B --> D["Retry Policy"]
  D --> E["Handle Exceptions"]
  E --> F["HttpRequestException"]
  E --> G["TaskCanceledException"]
  E --> H["TimeoutRejectedException"]
  E --> I["ApiException 503"]
  C --> J["Execute with Policy"]
  D --> J
  J --> K["CypherQueryAsync"]
Loading

Grey Divider

File Changes

1. src/Plugins/BotSharp.Plugin.Membase/GraphDb/MembaseGraphDb.cs ✨ Enhancement +44/-16

Implement retry policy with exponential backoff

• Added BuildRetryPolicy() method implementing exponential backoff retry logic with Polly
• Configured retry policy to handle HttpRequestException, TaskCanceledException,
 TimeoutRejectedException, and HTTP 503 errors
• Wrapped API calls with retry policy in ExecuteQueryAsync method
• Enhanced error handling with separate catch block for ApiException to log response content
• Simplified argument logging by serializing to JSON once instead of converting each argument
 individually
• Updated imports to use Polly and Refit namespaces

src/Plugins/BotSharp.Plugin.Membase/GraphDb/MembaseGraphDb.cs


2. src/Plugins/BotSharp.Plugin.Membase/MembasePlugin.cs ⚙️ Configuration changes +2/-1

Set fixed HTTP client timeout value

• Changed HTTP client timeout from dynamic settings.TimeoutSecond to fixed 90 seconds
• Added comment explaining timeout strategy between Refit client and graph database

src/Plugins/BotSharp.Plugin.Membase/MembasePlugin.cs


3. src/Plugins/BotSharp.Plugin.Membase/Settings/MembaseSettings.cs ⚙️ Configuration changes +1/-1

Increase default timeout setting

• Increased default TimeoutSecond from 10 to 30 seconds

src/Plugins/BotSharp.Plugin.Membase/Settings/MembaseSettings.cs


View more (2)
4. src/Plugins/BotSharp.Plugin.Membase/Using.cs Miscellaneous +9/-0

Add required global using statements

• Added global using statements for System.Net, System.Net.Http, System.Text.Json
• Added global using for Microsoft.Extensions.Logging
• Added global using statements for graph-related abstractions and Membase interfaces

src/Plugins/BotSharp.Plugin.Membase/Using.cs


5. src/Plugins/BotSharp.Plugin.Membase/BotSharp.Plugin.Membase.csproj Dependencies +1/-0

Add Polly package dependency

• Added Polly NuGet package dependency for retry and timeout policies

src/Plugins/BotSharp.Plugin.Membase/BotSharp.Plugin.Membase.csproj


Grey Divider

Qodo Logo

@qodo-code-review
Copy link
Copy Markdown
Contributor

qodo-code-review bot commented Mar 26, 2026

Code Review by Qodo

🐞 Bugs (4) 📘 Rule violations (1) 📎 Requirement gaps (0) 📐 Spec deviations (0)

Grey Divider


Action required

1. Logs ex.Content and query 📘 Rule violation ⛨ Security
Description
New error logging writes raw ex.Content, query, and serialized args (argLogs), which can
include sensitive user/model content and internal details. This increases risk of sensitive data
exposure through logs.
Code

src/Plugins/BotSharp.Plugin.Membase/GraphDb/MembaseGraphDb.cs[R79-82]

+        catch (ApiException ex)
+        {
+            _logger.LogError($"Error when executing query in {Provider} graph db:\r\n{ex.Content}\r\n{query}\r\n{argLogs}");
+            throw;
Evidence
PR Compliance ID 3 forbids logging sensitive user/model content or internal configuration; the added
logs include full query text, serialized arguments, and API error content (ex.Content), all of
which may contain sensitive data.

src/Plugins/BotSharp.Plugin.Membase/GraphDb/MembaseGraphDb.cs[60-60]
src/Plugins/BotSharp.Plugin.Membase/GraphDb/MembaseGraphDb.cs[79-83]
src/Plugins/BotSharp.Plugin.Membase/GraphDb/MembaseGraphDb.cs[86-86]
Best Practice: Learned patterns

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`MembaseGraphDb.ExecuteQueryAsync` logs raw `query`, serialized `args` (`argLogs`), and `ApiException.Content`, which may contain sensitive user/model content and/or internal configuration details.

## Issue Context
These values can contain user-provided text, identifiers, secrets, or verbose backend error payloads. Compliance requires minimizing and structuring logs to avoid sensitive content exposure.

## Fix Focus Areas
- src/Plugins/BotSharp.Plugin.Membase/GraphDb/MembaseGraphDb.cs[60-60]
- src/Plugins/BotSharp.Plugin.Membase/GraphDb/MembaseGraphDb.cs[79-83]
- src/Plugins/BotSharp.Plugin.Membase/GraphDb/MembaseGraphDb.cs[86-86]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Configured timeout not honored 🐞 Bug ⛯ Reliability
Description
BuildRetryPolicy computes a per-attempt timeout as TimeoutSecond/RetryCount and applies it inside a
retry policy with exponential backoff, so a single ExecuteQueryAsync can take significantly longer
than TimeoutSecond. This makes TimeoutSecond misleading and can cause unexpectedly long requests
under failure conditions.
Code

src/Plugins/BotSharp.Plugin.Membase/GraphDb/MembaseGraphDb.cs[R27-50]

+    private AsyncPolicy BuildRetryPolicy()
+    {
+        var settings = _services.GetRequiredService<MembaseSettings>();
+        var timeoutSeconds = (double)settings.TimeoutSecond / RetryCount;
+
+        var timeoutPolicy = Policy.TimeoutAsync(TimeSpan.FromSeconds(timeoutSeconds));
+
+        var retryPolicy = Policy
+            .Handle<HttpRequestException>()
+            .Or<TaskCanceledException>()
+            .Or<TimeoutRejectedException>()
+            .Or<ApiException>(ex => ex.StatusCode == HttpStatusCode.ServiceUnavailable)
+            .WaitAndRetryAsync(
+                retryCount: RetryCount,
+                sleepDurationProvider: retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
+                onRetry: (ex, timespan, retryAttempt, _) =>
+                {
+                    _logger.LogWarning(ex,
+                        "CypherQueryAsync retry {RetryAttempt}/{MaxRetries} after {Delay}s. Exception: {Message}",
+                        retryAttempt, RetryCount, timespan.TotalSeconds, ex.Message);
+                });
+
+        return Policy.WrapAsync(retryPolicy, timeoutPolicy);
+    }
Evidence
The policy sets per-try timeout to settings.TimeoutSecond/3, but then performs
WaitAndRetryAsync(retryCount: 3) with exponential delays and no outer “overall operation” timeout,
so total wall time can exceed the configured TimeoutSecond.

src/Plugins/BotSharp.Plugin.Membase/GraphDb/MembaseGraphDb.cs[25-50]
src/Plugins/BotSharp.Plugin.Membase/Settings/MembaseSettings.cs[5-12]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`MembaseGraphDb.BuildRetryPolicy` derives a per-attempt timeout from `TimeoutSecond / RetryCount` and wraps it inside a retry policy with backoff, allowing total execution time to exceed `TimeoutSecond` and making the setting misleading.

## Issue Context
- Current wrap applies timeout per attempt, not to the whole operation.
- Backoff delays add additional wall time.

## Fix Focus Areas
- src/Plugins/BotSharp.Plugin.Membase/GraphDb/MembaseGraphDb.cs[25-50]

## Suggested change
- Decide whether `TimeoutSecond` is **overall** timeout or **per-attempt** timeout.
- If it is overall: add an *outer* timeout policy for `TimeSpan.FromSeconds(settings.TimeoutSecond)` around the whole retry pipeline (and avoid dividing by retry count), or compute per-attempt timeout using `(RetryCount + 1)` and account for backoff.
- Consider renaming the constant to `MaxRetries` (retries) vs `MaxAttempts` (attempts) to avoid confusion.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Args serialization can break queries 🐞 Bug ✓ Correctness
Description
ExecuteQueryAsync serializes options.Arguments before entering the try/catch, so a serialization
failure will throw and prevent the Membase query from running. Since Arguments is
Dictionary<string, object>, callers can provide values that are not JSON-serializable.
Code

src/Plugins/BotSharp.Plugin.Membase/GraphDb/MembaseGraphDb.cs[R59-66]

        var args = options?.Arguments ?? new();
+        var argLogs = JsonSerializer.Serialize(args, BotSharpOptions.defaultJsonOptions);

        try
        {
-            var response = await _membaseApi.CypherQueryAsync(options!.GraphId, new CypherQueryRequest
-            {
-                Query = query,
-                Parameters = args
-            });
+            var retryPolicy = BuildRetryPolicy();
+            var response = await retryPolicy.ExecuteAsync(() =>
+                _membaseApi.CypherQueryAsync(options!.GraphId, new CypherQueryRequest
Evidence
argLogs is computed before the try block, but GraphQueryExecuteOptions.Arguments allows
arbitrary object values. Any JsonSerializer.Serialize exception will escape and abort the query
execution path.

src/Plugins/BotSharp.Plugin.Membase/GraphDb/MembaseGraphDb.cs[59-66]
src/Infrastructure/BotSharp.Abstraction/Graph/Options/GraphQueryOptions.cs[8-13]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`ExecuteQueryAsync` computes `argLogs` via `JsonSerializer.Serialize` before the `try` block. If serialization fails, the method throws before executing `_membaseApi.CypherQueryAsync`, even though `argLogs` is only used for logging.

## Issue Context
`GraphQueryExecuteOptions.Arguments` is `Dictionary<string, object>` so values can be non-serializable.

## Fix Focus Areas
- src/Plugins/BotSharp.Plugin.Membase/GraphDb/MembaseGraphDb.cs[59-61]
- src/Infrastructure/BotSharp.Abstraction/Graph/Options/GraphQueryOptions.cs[8-13]

## Suggested change
- Only serialize arguments inside the catch blocks (when you actually need to log), and wrap serialization in its own try/catch.
- Fallback to a safe placeholder (e.g., "<unserializable arguments>") if serialization fails.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

4. ApiException loses stack trace 🐞 Bug ✧ Quality
Description
The ApiException catch logs only a formatted string and does not pass the exception object to the
logger, so stack trace and exception details are lost. This reduces debuggability for Membase API
failures.
Code

src/Plugins/BotSharp.Plugin.Membase/GraphDb/MembaseGraphDb.cs[R79-83]

+        catch (ApiException ex)
+        {
+            _logger.LogError($"Error when executing query in {Provider} graph db:\r\n{ex.Content}\r\n{query}\r\n{argLogs}");
+            throw;
+        }
Evidence
_logger.LogError(...) is called without the ex parameter in the ApiException catch, unlike the
general Exception catch which logs with LogError(ex, ...).

src/Plugins/BotSharp.Plugin.Membase/GraphDb/MembaseGraphDb.cs[79-87]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The `catch (ApiException ex)` block logs using `LogError(string)` so the exception/stack trace is not captured.

## Issue Context
The generic `catch (Exception ex)` correctly uses `_logger.LogError(ex, ...)`.

## Fix Focus Areas
- src/Plugins/BotSharp.Plugin.Membase/GraphDb/MembaseGraphDb.cs[79-83]

## Suggested change
- Switch to `_logger.LogError(ex, "...", ...)`.
- Consider logging `ex.StatusCode` as a structured field and truncating `ex.Content` to a reasonable length to avoid massive log entries.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Hardcoded HttpClient timeout 🐞 Bug ⚙ Maintainability
Description
MembasePlugin now hardcodes the Refit HttpClient timeout to 90 seconds instead of using
MembaseSettings.TimeoutSecond, which can make configuration ineffective when users change
TimeoutSecond. This also diverges from MembaseGraphDb’s timeout logic which reads TimeoutSecond at
runtime.
Code

src/Plugins/BotSharp.Plugin.Membase/MembasePlugin.cs[R33-38]

                .ConfigureHttpClient(c =>
                {
                    c.BaseAddress = new Uri(settings.Host);
-                    c.Timeout = TimeSpan.FromSeconds(settings.TimeoutSecond);
+                    // Timeout is set by MembaseGrapbDb internally, but we set it here as well to ensure that the Refit client does not timeout before the graph db does.
+                    c.Timeout = TimeSpan.FromSeconds(90);
                });
Evidence
The plugin previously tied HttpClient timeout to settings.TimeoutSecond, but now uses a constant
90 seconds. Meanwhile MembaseGraphDb still derives its timeout values from
MembaseSettings.TimeoutSecond, so the two timeouts can drift as configuration changes.

src/Plugins/BotSharp.Plugin.Membase/MembasePlugin.cs[33-38]
src/Plugins/BotSharp.Plugin.Membase/GraphDb/MembaseGraphDb.cs[27-33]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`MembasePlugin` sets `HttpClient.Timeout` to a fixed 90s, while `MembaseGraphDb` derives timeouts from `MembaseSettings.TimeoutSecond`. This can make the `TimeoutSecond` setting confusing/ineffective.

## Issue Context
The comment says timeout is set internally by MembaseGraphDb, but the client timeout is still an independent limiter.

## Fix Focus Areas
- src/Plugins/BotSharp.Plugin.Membase/MembasePlugin.cs[33-38]
- src/Plugins/BotSharp.Plugin.Membase/GraphDb/MembaseGraphDb.cs[27-33]

## Suggested change
- Either:
 - Set `HttpClient.Timeout` based on `settings.TimeoutSecond` (+ backoff buffer), or
 - Set `HttpClient.Timeout = Timeout.InfiniteTimeSpan` and rely on Polly to enforce timeouts consistently.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

Comment on lines +79 to +82
catch (ApiException ex)
{
_logger.LogError($"Error when executing query in {Provider} graph db:\r\n{ex.Content}\r\n{query}\r\n{argLogs}");
throw;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Action required

1. Logs ex.content and query 📘 Rule violation ⛨ Security

New error logging writes raw ex.Content, query, and serialized args (argLogs), which can
include sensitive user/model content and internal details. This increases risk of sensitive data
exposure through logs.
Agent Prompt
## Issue description
`MembaseGraphDb.ExecuteQueryAsync` logs raw `query`, serialized `args` (`argLogs`), and `ApiException.Content`, which may contain sensitive user/model content and/or internal configuration details.

## Issue Context
These values can contain user-provided text, identifiers, secrets, or verbose backend error payloads. Compliance requires minimizing and structuring logs to avoid sensitive content exposure.

## Fix Focus Areas
- src/Plugins/BotSharp.Plugin.Membase/GraphDb/MembaseGraphDb.cs[60-60]
- src/Plugins/BotSharp.Plugin.Membase/GraphDb/MembaseGraphDb.cs[79-83]
- src/Plugins/BotSharp.Plugin.Membase/GraphDb/MembaseGraphDb.cs[86-86]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +27 to +50
private AsyncPolicy BuildRetryPolicy()
{
var settings = _services.GetRequiredService<MembaseSettings>();
var timeoutSeconds = (double)settings.TimeoutSecond / RetryCount;

var timeoutPolicy = Policy.TimeoutAsync(TimeSpan.FromSeconds(timeoutSeconds));

var retryPolicy = Policy
.Handle<HttpRequestException>()
.Or<TaskCanceledException>()
.Or<TimeoutRejectedException>()
.Or<ApiException>(ex => ex.StatusCode == HttpStatusCode.ServiceUnavailable)
.WaitAndRetryAsync(
retryCount: RetryCount,
sleepDurationProvider: retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
onRetry: (ex, timespan, retryAttempt, _) =>
{
_logger.LogWarning(ex,
"CypherQueryAsync retry {RetryAttempt}/{MaxRetries} after {Delay}s. Exception: {Message}",
retryAttempt, RetryCount, timespan.TotalSeconds, ex.Message);
});

return Policy.WrapAsync(retryPolicy, timeoutPolicy);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Action required

2. Configured timeout not honored 🐞 Bug ⛯ Reliability

BuildRetryPolicy computes a per-attempt timeout as TimeoutSecond/RetryCount and applies it inside a
retry policy with exponential backoff, so a single ExecuteQueryAsync can take significantly longer
than TimeoutSecond. This makes TimeoutSecond misleading and can cause unexpectedly long requests
under failure conditions.
Agent Prompt
## Issue description
`MembaseGraphDb.BuildRetryPolicy` derives a per-attempt timeout from `TimeoutSecond / RetryCount` and wraps it inside a retry policy with backoff, allowing total execution time to exceed `TimeoutSecond` and making the setting misleading.

## Issue Context
- Current wrap applies timeout per attempt, not to the whole operation.
- Backoff delays add additional wall time.

## Fix Focus Areas
- src/Plugins/BotSharp.Plugin.Membase/GraphDb/MembaseGraphDb.cs[25-50]

## Suggested change
- Decide whether `TimeoutSecond` is **overall** timeout or **per-attempt** timeout.
- If it is overall: add an *outer* timeout policy for `TimeSpan.FromSeconds(settings.TimeoutSecond)` around the whole retry pipeline (and avoid dividing by retry count), or compute per-attempt timeout using `(RetryCount + 1)` and account for backoff.
- Consider renaming the constant to `MaxRetries` (retries) vs `MaxAttempts` (attempts) to avoid confusion.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines 59 to +66
var args = options?.Arguments ?? new();
var argLogs = JsonSerializer.Serialize(args, BotSharpOptions.defaultJsonOptions);

try
{
var response = await _membaseApi.CypherQueryAsync(options!.GraphId, new CypherQueryRequest
{
Query = query,
Parameters = args
});
var retryPolicy = BuildRetryPolicy();
var response = await retryPolicy.ExecuteAsync(() =>
_membaseApi.CypherQueryAsync(options!.GraphId, new CypherQueryRequest
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Action required

3. Args serialization can break queries 🐞 Bug ✓ Correctness

ExecuteQueryAsync serializes options.Arguments before entering the try/catch, so a serialization
failure will throw and prevent the Membase query from running. Since Arguments is
Dictionary<string, object>, callers can provide values that are not JSON-serializable.
Agent Prompt
## Issue description
`ExecuteQueryAsync` computes `argLogs` via `JsonSerializer.Serialize` before the `try` block. If serialization fails, the method throws before executing `_membaseApi.CypherQueryAsync`, even though `argLogs` is only used for logging.

## Issue Context
`GraphQueryExecuteOptions.Arguments` is `Dictionary<string, object>` so values can be non-serializable.

## Fix Focus Areas
- src/Plugins/BotSharp.Plugin.Membase/GraphDb/MembaseGraphDb.cs[59-61]
- src/Infrastructure/BotSharp.Abstraction/Graph/Options/GraphQueryOptions.cs[8-13]

## Suggested change
- Only serialize arguments inside the catch blocks (when you actually need to log), and wrap serialization in its own try/catch.
- Fallback to a safe placeholder (e.g., "<unserializable arguments>") if serialization fails.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@Oceania2018 Oceania2018 merged commit 00c85b4 into SciSharp:master Mar 26, 2026
4 checks passed
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.

3 participants