Repository: Altinn/altinn-broker
Pull request #723: Retry transient npgsql exceptions for DB operations
Author: login: axely123
Labels: name: kind/bug
description: Something isn't working
Created: 2025-05-12T13:17:57Z | Merged: 2025-05-14T07:31:10Z

<!--- Provide a general summary of your changes in the Title above -->

## Description
<!--- Describe your changes in detail -->
We have experienced some occasional timeout exceptions when reading from stream with npgsql as well as some other seemingly transient exceptions. This PR adds a retry mechanism for transient npgsql exceptions and postgresexceptions to command executions in repositories. The retries are limited to 3.

The retry mechanism for transient npgsql exceptions was added to the command execution in repositories instead of on transactions because then the whole transaction doesn't have to rollback and be retried on one transient error. Also not all repository calls are used in a transaction with a retry policy.

## Related Issue(s)
- #719 

## Verification
- [x] **Your** code builds clean without any errors or warnings
- [x] Manual testing done (required)
- [x] Relevant automated test added (if you find this hard, leave it and we'll help out)
- [x] All tests run green

## Documentation
- [x] User documentation is updated with a separate linked PR in [altinn-studio-docs.](https://github.com/Altinn/altinn-studio-docs) (if applicable)


<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Introduced automatic retry handling for database operations to improve resilience against transient failures.

- **Refactor**
  - Updated all repositories to use a new retry mechanism for executing database commands, enhancing reliability without altering existing functionality.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/src/Altinn.Broker.Persistence/DependencyInjection.cs b/src/Altinn.Broker.Persistence/DependencyInjection.cs
index e0330c77..1d393772 100644
--- a/src/Altinn.Broker.Persistence/DependencyInjection.cs
+++ b/src/Altinn.Broker.Persistence/DependencyInjection.cs
@@ -1,4 +1,5 @@
 ﻿using Altinn.Broker.Core.Repositories;
+using Altinn.Broker.Persistence.Helpers;
 using Altinn.Broker.Persistence.Options;
 using Altinn.Broker.Persistence.Repositories;
 
@@ -16,6 +17,7 @@ public static class DependencyInjection
     public static void AddPersistence(this IServiceCollection services, IConfiguration config)
     {
         services.AddSingleton(BuildAzureNpgsqlDataSource(config));
+        services.AddSingleton<ExecuteDBCommandWithRetries>();
         services.AddSingleton<IActorRepository, ActorRepository>();
         services.AddSingleton<IFileTransferRepository, FileTransferRepository>();
         services.AddSingleton<IFileTransferStatusRepository, FileTransferStatusRepository>();
diff --git a/src/Altinn.Broker.Persistence/Helpers/ExecuteDBCommandWithRetries.cs b/src/Altinn.Broker.Persistence/Helpers/ExecuteDBCommandWithRetries.cs
new file mode 100644
index 00000000..32036216
--- /dev/null
+++ b/src/Altinn.Broker.Persistence/Helpers/ExecuteDBCommandWithRetries.cs
@@ -0,0 +1,71 @@
+using Microsoft.Extensions.Logging;
+using Npgsql;
+using Polly;
+using Polly.Retry;
+
+namespace Altinn.Broker.Persistence.Helpers;
+
+/// <summary>
+/// Helper class to execute database commands with retry logic for transient failures
+/// </summary>
+public class ExecuteDBCommandWithRetries(ILogger<ExecuteDBCommandWithRetries> logger)
+{
+    private readonly ILogger<ExecuteDBCommandWithRetries> _logger = logger;
+
+    /// <summary>
+    /// Executes a database command with retry logic for transient failures
+    /// </summary>
+    /// <typeparam name="T">Return type of the operation</typeparam>
+    /// <param name="operation">The operation to execute</param>
+    /// <param name="cancellationToken">Cancellation token</param>
+    /// <returns>Result of the operation</returns>
+    public async Task<T> ExecuteWithRetry<T>(Func<CancellationToken, Task<T>> operation, CancellationToken cancellationToken = default)
+    {
+        var result = await CreateRetryPolicy().ExecuteAndCaptureAsync(operation, cancellationToken);
+        
+        if (result.Outcome == OutcomeType.Failure)
+        {
+            _logger.LogError("Exception during database command retries: {message}\n{stackTrace}", 
+                result.FinalException.Message, 
+                result.FinalException.StackTrace);
+            throw result.FinalException;
+        }
+        
+        return result.Result;
+    }
+
+    /// <summary>
+    /// Executes a database command that returns no result with retry logic for transient failures
+    /// </summary>
+    /// <param name="operation">The operation to execute</param>
+    /// <param name="cancellationToken">Cancellation token</param>
+    public async Task ExecuteWithRetry(Func<CancellationToken, Task> operation, CancellationToken cancellationToken = default)
+    {
+        var policy = CreateRetryPolicy();
+        var result = await policy.ExecuteAndCaptureAsync(operation, cancellationToken);
+        
+        if (result.Outcome == OutcomeType.Failure)
+        {
+            _logger.LogError("Exception during database command retries: {message}\n{stackTrace}", 
+                result.FinalException.Message, 
+                result.FinalException.StackTrace);
+            throw result.FinalException;
+        }
+    }
+
+    private AsyncRetryPolicy CreateRetryPolicy()
+    {
+        return Policy
+            .Handle<NpgsqlException>(ex => ex.IsTransient && ex is not PostgresException)
+            .WaitAndRetryAsync(
+                3,
+                retryAttempt => TimeSpan.FromMilliseconds(100),
+                (exception, timeSpan, retryCount, context) =>
+                {
+                    _logger.LogWarning("Database command attempt {RetryCount} failed with exception: {ExceptionMessage}. Retrying in {RetryTimeSpan}ms...",
+                        retryCount,
+                        exception.Message,
+                        timeSpan.TotalMilliseconds);
+                });
+    }
+} 
\ No newline at end of file
diff --git a/src/Altinn.Broker.Persistence/Repositories/ActorFileTransferStatusRepository.cs b/src/Altinn.Broker.Persistence/Repositories/ActorFileTransferStatusRepository.cs
index 0ebe867a..76955373 100644
--- a/src/Altinn.Broker.Persistence/Repositories/ActorFileTransferStatusRepository.cs
+++ b/src/Altinn.Broker.Persistence/Repositories/ActorFileTransferStatusRepository.cs
@@ -1,11 +1,12 @@
 ﻿using Altinn.Broker.Core.Domain;
 using Altinn.Broker.Core.Domain.Enums;
 using Altinn.Broker.Core.Repositories;
+using Altinn.Broker.Persistence.Helpers;
 
 using Npgsql;
 
 namespace Altinn.Broker.Persistence.Repositories;
-internal class ActorFileTransferStatusRepository(IActorRepository actorRepository, NpgsqlDataSource dataSource) : IActorFileTransferStatusRepository
+internal class ActorFileTransferStatusRepository(IActorRepository actorRepository, NpgsqlDataSource dataSource, ExecuteDBCommandWithRetries commandExecutor) : IActorFileTransferStatusRepository
 {
     public async Task<List<ActorFileTransferStatusEntity>> GetActorEvents(Guid fileTransferId, CancellationToken cancellationToken)
     {
@@ -15,10 +16,12 @@ public async Task<List<ActorFileTransferStatusEntity>> GetActorEvents(Guid fileT
            "INNER JOIN broker.actor a on a.actor_id_pk = afs.actor_id_fk " +
            "WHERE afs.file_transfer_id_fk = @fileTransferId");
         command.Parameters.AddWithValue("@fileTransferId", fileTransferId);
-        var fileTransferStatuses = new List<ActorFileTransferStatusEntity>();
-        using (var reader = await command.ExecuteReaderAsync(cancellationToken))
+        
+        return await commandExecutor.ExecuteWithRetry(async (ct) =>
         {
-            while (await reader.ReadAsync(cancellationToken))
+            var fileTransferStatuses = new List<ActorFileTransferStatusEntity>();
+            await using var reader = await command.ExecuteReaderAsync(ct);
+            while (await reader.ReadAsync(ct))
             {
                 fileTransferStatuses.Add(new Core.Domain.ActorFileTransferStatusEntity()
                 {
@@ -32,8 +35,8 @@ public async Task<List<ActorFileTransferStatusEntity>> GetActorEvents(Guid fileT
                     }
                 });
             }
-        }
-        return fileTransferStatuses;
+            return fileTransferStatuses;
+        }, cancellationToken);
     }
 
     public async Task InsertActorFileTransferStatus(Guid fileTransferId, ActorFileTransferStatus status, string actorExternalReference, CancellationToken cancellationToken)
@@ -51,12 +54,14 @@ public async Task InsertActorFileTransferStatus(Guid fileTransferId, ActorFileTr
         {
             actorId = actor.ActorId;
         }
+        
         await using var command = dataSource.CreateCommand(
             "INSERT INTO broker.actor_file_transfer_status (actor_id_fk, file_transfer_id_fk, actor_file_transfer_status_description_id_fk, actor_file_transfer_status_date) " +
             "VALUES (@actorId, @fileTransferId, @actorFileTransferStatusId, NOW())");
         command.Parameters.AddWithValue("@actorId", actorId);
         command.Parameters.AddWithValue("@fileTransferId", fileTransferId);
         command.Parameters.AddWithValue("@actorFileTransferStatusId", (int)status);
-        await command.ExecuteNonQueryAsync(cancellationToken);
+        
+        await commandExecutor.ExecuteWithRetry(command.ExecuteNonQueryAsync, cancellationToken);
     }
 }
diff --git a/src/Altinn.Broker.Persistence/Repositories/ActorRepository.cs b/src/Altinn.Broker.Persistence/Repositories/ActorRepository.cs
index 99951094..ca68c9af 100644
--- a/src/Altinn.Broker.Persistence/Repositories/ActorRepository.cs
+++ b/src/Altinn.Broker.Persistence/Repositories/ActorRepository.cs
@@ -1,11 +1,12 @@
 ﻿using Altinn.Broker.Core.Domain;
 using Altinn.Broker.Core.Repositories;
+using Altinn.Broker.Persistence.Helpers;
 
 using Npgsql;
 
 namespace Altinn.Broker.Persistence.Repositories;
 
-public class ActorRepository(NpgsqlDataSource dataSource) : IActorRepository
+public class ActorRepository(NpgsqlDataSource dataSource, ExecuteDBCommandWithRetries commandExecutor) : IActorRepository
 {
     public async Task<ActorEntity?> GetActorAsync(string actorExternalId, CancellationToken cancellationToken)
     {
@@ -13,18 +14,20 @@ public class ActorRepository(NpgsqlDataSource dataSource) : IActorRepository
         "SELECT actor_id_pk, actor_external_id FROM broker.actor WHERE actor_external_id = @actorExternalId");
         command.Parameters.AddWithValue("@actorExternalId", actorExternalId);
 
-        await using NpgsqlDataReader reader = await command.ExecuteReaderAsync(cancellationToken);
-        ActorEntity? actor = null;
-        while (await reader.ReadAsync(cancellationToken))
+        return await commandExecutor.ExecuteWithRetry(async (ct) =>
         {
-            actor = new ActorEntity
+            ActorEntity? actor = null;
+            await using var reader = await command.ExecuteReaderAsync(ct);
+            while (await reader.ReadAsync(ct))
             {
-                ActorId = reader.GetInt32(reader.GetOrdinal("actor_id_pk")),
-                ActorExternalId = reader.GetString(reader.GetOrdinal("actor_external_id"))
-            };
-        }
-        return actor;
-
+                actor = new ActorEntity
+                {
+                    ActorId = reader.GetInt32(reader.GetOrdinal("actor_id_pk")),
+                    ActorExternalId = reader.GetString(reader.GetOrdinal("actor_external_id"))
+                };
+            }
+            return actor;
+        }, cancellationToken);
     }
 
     public async Task<long> AddActorAsync(ActorEntity actor, CancellationToken cancellationToken)
@@ -35,6 +38,8 @@ public async Task<long> AddActorAsync(ActorEntity actor, CancellationToken cance
                 "RETURNING actor_id_pk");
         command.Parameters.AddWithValue("@actorExternalId", actor.ActorExternalId);
 
-        return (long)(await command.ExecuteScalarAsync(cancellationToken))!;
+        return await commandExecutor.ExecuteWithRetry(async (ct) => 
+            (long)(await command.ExecuteScalarAsync(ct))!, 
+            cancellationToken);
     }
 }
diff --git a/src/Altinn.Broker.Persistence/Repositories/FileTransferRepository.cs b/src/Altinn.Broker.Persistence/Repositories/FileTransferRepository.cs
index 257a967b..61b36be6 100644
--- a/src/Altinn.Broker.Persistence/Repositories/FileTransferRepository.cs
+++ b/src/Altinn.Broker.Persistence/Repositories/FileTransferRepository.cs
@@ -3,6 +3,7 @@
 using Altinn.Broker.Core.Domain;
 using Altinn.Broker.Core.Domain.Enums;
 using Altinn.Broker.Core.Repositories;
+using Altinn.Broker.Persistence.Helpers;
 
 using Npgsql;
 
@@ -12,12 +13,10 @@
 
 namespace Altinn.Broker.Persistence.Repositories;
 
-public class FileTransferRepository(NpgsqlDataSource dataSource, IActorRepository actorRepository) : IFileTransferRepository
+public class FileTransferRepository(NpgsqlDataSource dataSource, IActorRepository actorRepository, ExecuteDBCommandWithRetries commandExecutor) : IFileTransferRepository
 {
     public async Task<FileTransferEntity?> GetFileTransfer(Guid fileTransferId, CancellationToken cancellationToken)
     {
-        FileTransferEntity fileTransfer;
-
         await using var command = dataSource.CreateCommand(
             @"
                 SELECT 
@@ -65,10 +64,16 @@ GROUP BY
                     ) fs_latest ON f.file_transfer_id_pk = fs_latest.file_transfer_id_fk
                 WHERE 
                     f.file_transfer_id_pk = @fileTransferId;");
+        
+        command.Parameters.AddWithValue("@fileTransferId", fileTransferId);
+        
+        return await commandExecutor.ExecuteWithRetry(async (ct) => 
         {
-            command.Parameters.AddWithValue("@fileTransferId", fileTransferId);
-            await using NpgsqlDataReader reader = await command.ExecuteReaderAsync(cancellationToken);
-            if (await reader.ReadAsync(cancellationToken))
+            FileTransferEntity? fileTransfer = null;
+            
+            await using var reader = await command.ExecuteReaderAsync(ct);
+            
+            if (await reader.ReadAsync(ct))
             {
                 fileTransfer = new FileTransferEntity
                 {
@@ -95,18 +100,16 @@ GROUP BY
                         ActorId = reader.GetInt64(reader.GetOrdinal("sender_actor_id_fk")),
                         ActorExternalId = reader.GetString(reader.GetOrdinal("senderActorExternalReference"))
                     },
-                    RecipientCurrentStatuses = await GetLatestRecipientFileTransferStatuses(fileTransferId, cancellationToken),
-                    PropertyList = await GetMetadata(fileTransferId, cancellationToken),
+                    RecipientCurrentStatuses = await GetLatestRecipientFileTransferStatuses(fileTransferId, ct),
+                    PropertyList = await GetMetadata(fileTransferId, ct),
                     UseVirusScan = reader.GetBoolean(reader.GetOrdinal("use_virus_scan"))
                 };
+                
+                EnrichLogs(fileTransfer);
             }
-            else
-            {
-                return null;
-            }
-        }
-        EnrichLogs(fileTransfer);
-        return fileTransfer;
+
+            return fileTransfer;
+        }, cancellationToken);
     }
 
     private static void EnrichLogs(FileTransferEntity fileTransferEntity)
@@ -124,8 +127,7 @@ private static void EnrichLogs(FileTransferEntity fileTransferEntity)
      * */
     private async Task<List<ActorFileTransferStatusEntity>> GetLatestRecipientFileTransferStatuses(Guid fileTransferId, CancellationToken cancellationToken)
     {
-        var fileTransferStatuses = new List<ActorFileTransferStatusEntity>();
-        await using (var command = dataSource.CreateCommand(
+        await using var command = dataSource.CreateCommand(
             @"
             SELECT afs.actor_id_fk, MAX(afs.actor_file_transfer_status_description_id_fk) as actor_file_transfer_status_description_id_fk, MAX(afs.actor_file_transfer_status_date) as actor_file_transfer_status_date, a.actor_external_id 
             FROM broker.file_transfer 
@@ -133,12 +135,16 @@ FROM broker.file_transfer
             LEFT JOIN broker.actor a on a.actor_id_pk = afs.actor_id_fk 
             WHERE file_transfer_id_pk = @fileTransferId 
             GROUP BY afs.actor_id_fk, a.actor_external_id
-        "))
+        ");
+        
+        command.Parameters.AddWithValue("@fileTransferId", fileTransferId);
+        
+        return await commandExecutor.ExecuteWithRetry(async (ct) =>
         {
-            command.Parameters.AddWithValue("@fileTransferId", fileTransferId);
-            var commandText = command.CommandText;
-            await using var reader = await command.ExecuteReaderAsync(cancellationToken);
-            while (await reader.ReadAsync(cancellationToken))
+            var fileTransferStatuses = new List<ActorFileTransferStatusEntity>();
+            await using var reader = await command.ExecuteReaderAsync(ct);
+                
+            while (await reader.ReadAsync(ct))
             {
                 fileTransferStatuses.Add(new ActorFileTransferStatusEntity()
                 {
@@ -152,8 +158,9 @@ FROM broker.file_transfer
                     }
                 });
             }
-        }
-        return fileTransferStatuses;
+
+            return fileTransferStatuses;
+        }, cancellationToken);
     }
 
     public async Task<Guid> AddFileTransfer(ResourceEntity resource, StorageProviderEntity storageProviderEntity, string fileName, string sendersFileTransferReference, string senderExternalId, List<string> recipientIds, DateTimeOffset expirationTime, Dictionary<string, string> propertyList, string? checksum, bool useVirusScan, CancellationToken cancellationToken = default)
@@ -171,6 +178,7 @@ public async Task<Guid> AddFileTransfer(ResourceEntity resource, StorageProvider
         {
             actorId = actor.ActorId;
         }
+            
         var fileTransferId = Guid.NewGuid();
         await using NpgsqlCommand command = dataSource.CreateCommand(
             "INSERT INTO broker.file_transfer (file_transfer_id_pk, resource_id, filename, checksum, file_transfer_size, external_file_transfer_reference, sender_actor_id_fk, created, storage_provider_id_fk, expiration_time, hangfire_job_id, use_virus_scan) " +
@@ -189,7 +197,7 @@ public async Task<Guid> AddFileTransfer(ResourceEntity resource, StorageProvider
         command.Parameters.AddWithValue("@expirationTime", expirationTime);
         command.Parameters.AddWithValue("@useVirusScan", useVirusScan);
 
-        await command.ExecuteNonQueryAsync(cancellationToken);
+        await commandExecutor.ExecuteWithRetry(command.ExecuteNonQueryAsync, cancellationToken);
 
         await SetMetadata(fileTransferId, propertyList, cancellationToken);
         return fileTransferId;
@@ -269,18 +277,19 @@ public async Task<List<Guid>> LegacyGetFilesForRecipientsWithRecipientStatus(Leg
         if (fileTransferSearch.FileTransferStatus.HasValue)
             command.Parameters.AddWithValue("@fileTransferStatus", (int)fileTransferSearch.FileTransferStatus);
 
-
-
-        var fileTransfers = new List<Guid>();
-        await using (var reader = await command.ExecuteReaderAsync(cancellationToken))
+        return await commandExecutor.ExecuteWithRetry(async (ct) =>
         {
-            while (await reader.ReadAsync(cancellationToken))
+            var fileTransfers = new List<Guid>();
+            
+            await using var reader = await command.ExecuteReaderAsync(ct);
+            while (await reader.ReadAsync(ct))
             {
                 var fileTransferId = reader.GetGuid(0);
                 fileTransfers.Add(fileTransferId);
             }
-        }
-        return fileTransfers;
+            
+            return fileTransfers;
+        }, cancellationToken);
     }
 
     public async Task<List<Guid>> GetFileTransfersAssociatedWithActor(FileTransferSearchEntity fileTransferSearch, CancellationToken cancellationToken)
@@ -327,29 +336,30 @@ ORDER BY created DESC
 
         commandString = string.Format(commandString, statusCondition, dateCondition);
 
-        await using (var command = dataSource.CreateCommand(commandString))
-        {
-            command.Parameters.AddWithValue("@actorId", fileTransferSearch.Actor.ActorId);
-            command.Parameters.AddWithValue("@resourceId", fileTransferSearch.ResourceId);
-            command.Parameters.AddWithValue("@actorExternalId", fileTransferSearch.Actor.ActorExternalId);
-            if (fileTransferSearch.From.HasValue)
-                command.Parameters.AddWithValue("@From", fileTransferSearch.From);
-            if (fileTransferSearch.To.HasValue)
-                command.Parameters.AddWithValue("@To", fileTransferSearch.To);
-            if (fileTransferSearch.Status.HasValue)
-                command.Parameters.AddWithValue("@fileTransferStatus", (int)fileTransferSearch.Status);
+        await using var command = dataSource.CreateCommand(commandString);
+        command.Parameters.AddWithValue("@actorId", fileTransferSearch.Actor.ActorId);
+        command.Parameters.AddWithValue("@resourceId", fileTransferSearch.ResourceId);
+        command.Parameters.AddWithValue("@actorExternalId", fileTransferSearch.Actor.ActorExternalId);
+        if (fileTransferSearch.From.HasValue)
+            command.Parameters.AddWithValue("@From", fileTransferSearch.From);
+        if (fileTransferSearch.To.HasValue)
+            command.Parameters.AddWithValue("@To", fileTransferSearch.To);
+        if (fileTransferSearch.Status.HasValue)
+            command.Parameters.AddWithValue("@fileTransferStatus", (int)fileTransferSearch.Status);
 
+        return await commandExecutor.ExecuteWithRetry(async (ct) =>
+        {
             var files = new List<Guid>();
-            await using (var reader = await command.ExecuteReaderAsync(cancellationToken))
+            
+            await using var reader = await command.ExecuteReaderAsync(ct);
+            while (await reader.ReadAsync(ct))
             {
-                while (await reader.ReadAsync(cancellationToken))
-                {
-                    var fileTransferId = reader.GetGuid(0);
-                    files.Add(fileTransferId);
-                }
+                var fileTransferId = reader.GetGuid(0);
+                files.Add(fileTransferId);
             }
+            
             return files;
-        }
+        }, cancellationToken);
     }
 
     public async Task<List<Guid>> GetFileTransfersForRecipientWithRecipientStatus(FileTransferSearchEntity fileTransferSearch, CancellationToken cancellationToken)
@@ -397,31 +407,33 @@ ORDER BY created DESC
 
         commandString = string.Format(commandString, statusCondition, dateCondition);
 
-        await using (var command = dataSource.CreateCommand(commandString))
-        {
-            command.Parameters.AddWithValue("@recipientId", fileTransferSearch.Actor.ActorId);
-            command.Parameters.AddWithValue("@resourceId", fileTransferSearch.ResourceId);
-            command.Parameters.AddWithValue("@recipientFileStatus", (int)fileTransferSearch.RecipientStatus);
+        await using var command = dataSource.CreateCommand(commandString);
+        command.Parameters.AddWithValue("@recipientId", fileTransferSearch.Actor.ActorId);
+        command.Parameters.AddWithValue("@resourceId", fileTransferSearch.ResourceId);
+        command.Parameters.AddWithValue("@recipientFileStatus", (int)fileTransferSearch.RecipientStatus);
 
-            if (fileTransferSearch.From.HasValue)
-                command.Parameters.AddWithValue("@from", fileTransferSearch.From);
-            if (fileTransferSearch.To.HasValue)
-                command.Parameters.AddWithValue("@to", fileTransferSearch.To);
-            if (fileTransferSearch.Status.HasValue)
-                command.Parameters.AddWithValue("@fileStatus", (int)fileTransferSearch.Status);
+        if (fileTransferSearch.From.HasValue)
+            command.Parameters.AddWithValue("@from", fileTransferSearch.From);
+        if (fileTransferSearch.To.HasValue)
+            command.Parameters.AddWithValue("@to", fileTransferSearch.To);
+        if (fileTransferSearch.Status.HasValue)
+            command.Parameters.AddWithValue("@fileStatus", (int)fileTransferSearch.Status);
 
+        return await commandExecutor.ExecuteWithRetry(async (ct) =>
+        {
             var files = new List<Guid>();
-            await using (var reader = await command.ExecuteReaderAsync(cancellationToken))
+            
+            await using var reader = await command.ExecuteReaderAsync(ct);
+            while (await reader.ReadAsync(ct))
             {
-                while (await reader.ReadAsync(cancellationToken))
-                {
-                    var fileTransferId = reader.GetGuid(0);
-                    files.Add(fileTransferId);
-                }
+                var fileTransferId = reader.GetGuid(0);
+                files.Add(fileTransferId);
             }
+            
             return files;
-        }
+        }, cancellationToken);
     }
+    
     public async Task SetStorageDetails(Guid fileTransferId, long storageProviderId, string fileLocation, long filesize, CancellationToken cancellationToken)
     {
         await using (var command = dataSource.CreateCommand(
@@ -436,26 +448,26 @@ public async Task SetStorageDetails(Guid fileTransferId, long storageProviderId,
             command.Parameters.AddWithValue("@storageProviderId", storageProviderId);
             command.Parameters.AddWithValue("@fileLocation", fileLocation);
             command.Parameters.AddWithValue("@filesize", filesize);
-            await command.ExecuteNonQueryAsync(cancellationToken);
+            await commandExecutor.ExecuteWithRetry(command.ExecuteNonQueryAsync, cancellationToken);
         }
     }
 
     private async Task<Dictionary<string, string>> GetMetadata(Guid fileTransferId, CancellationToken cancellationToken)
     {
-        await using var command = dataSource.CreateCommand(
-            "SELECT * " +
-            "FROM broker.file_transfer_property " +
-            "WHERE file_transfer_id_fk = @fileTransferId");
-        command.Parameters.AddWithValue("@fileTransferId", fileTransferId);
-        var property = new Dictionary<string, string>();
-        await using (var reader = await command.ExecuteReaderAsync(cancellationToken))
+        await using var command = dataSource.CreateCommand("SELECT key, value FROM broker.file_transfer_property WHERE file_transfer_id_fk = @filetransferId");
+        command.Parameters.AddWithValue("@filetransferId", fileTransferId);
+        
+        return await commandExecutor.ExecuteWithRetry(async (ct) =>
         {
-            while (await reader.ReadAsync(cancellationToken))
+            var metadata = new Dictionary<string, string>();
+            await using var reader = await command.ExecuteReaderAsync(ct);
+                
+            while (await reader.ReadAsync(ct))
             {
-                property.Add(reader.GetString(reader.GetOrdinal("key")), reader.GetString(reader.GetOrdinal("value")));
+                metadata.Add(reader.GetString(reader.GetOrdinal("key")), reader.GetString(reader.GetOrdinal("value")));
             }
-        }
-        return property;
+            return metadata;
+        }, cancellationToken);
     }
 
     private async Task SetMetadata(Guid fileTransferId, Dictionary<string, string> property, CancellationToken cancellationToken)
@@ -503,7 +515,7 @@ public async Task SetChecksum(Guid fileTransferId, string checksum, Cancellation
         {
             command.Parameters.AddWithValue("@fileTransferId", fileTransferId);
             command.Parameters.AddWithValue("@checksum", checksum);
-            command.ExecuteNonQuery();
+            await commandExecutor.ExecuteWithRetry(command.ExecuteNonQueryAsync, cancellationToken);
         }
     }
     public async Task SetFileTransferHangfireJobId(Guid fileTransferId, string hangfireJobId, CancellationToken cancellationToken)
@@ -516,7 +528,7 @@ public async Task SetFileTransferHangfireJobId(Guid fileTransferId, string hangf
         {
             command.Parameters.AddWithValue("@fileTransferId", fileTransferId);
             command.Parameters.AddWithValue("@hangfireJobId", hangfireJobId is null ? DBNull.Value : hangfireJobId);
-            command.ExecuteNonQuery();
+            await commandExecutor.ExecuteWithRetry(command.ExecuteNonQueryAsync, cancellationToken);
         }
     }
 }
diff --git a/src/Altinn.Broker.Persistence/Repositories/FileTransferStatusRepository.cs b/src/Altinn.Broker.Persistence/Repositories/FileTransferStatusRepository.cs
index 90bd7bdd..b3b3674e 100644
--- a/src/Altinn.Broker.Persistence/Repositories/FileTransferStatusRepository.cs
+++ b/src/Altinn.Broker.Persistence/Repositories/FileTransferStatusRepository.cs
@@ -1,11 +1,12 @@
 ﻿using Altinn.Broker.Core.Domain;
 using Altinn.Broker.Core.Domain.Enums;
 using Altinn.Broker.Core.Repositories;
+using Altinn.Broker.Persistence.Helpers;
 
 using Npgsql;
 
 namespace Altinn.Broker.Persistence.Repositories;
-public class FileTransferStatusRepository(NpgsqlDataSource dataSource) : IFileTransferStatusRepository
+public class FileTransferStatusRepository(NpgsqlDataSource dataSource, ExecuteDBCommandWithRetries commandExecutor) : IFileTransferStatusRepository
 {
     public async Task InsertFileTransferStatus(Guid fileTransferId, FileTransferStatus status, string? detailedFileTransferStatus = null, CancellationToken cancellationToken = default)
     {
@@ -16,7 +17,8 @@ public async Task InsertFileTransferStatus(Guid fileTransferId, FileTransferStat
         command.Parameters.AddWithValue("@statusId", (int)status);
         command.Parameters.AddWithValue("@detailedFileTransferStatus", detailedFileTransferStatus is null ? DBNull.Value : detailedFileTransferStatus);
 
-        var fileTransferStatusId = await command.ExecuteScalarAsync(cancellationToken);
+        var fileTransferStatusId = await commandExecutor.ExecuteWithRetry(command.ExecuteScalarAsync, cancellationToken);
+            
         if (fileTransferStatusId == null)
         {
             throw new InvalidOperationException("No file_transfer_status_id_pk was returned after insert.");
@@ -30,10 +32,12 @@ public async Task<List<FileTransferStatusEntity>> GetFileTransferStatusHistory(G
             "FROM broker.file_transfer_status fis " +
             "WHERE fis.file_transfer_id_fk = @fileTransferId");
         command.Parameters.AddWithValue("@fileTransferId", fileTransferId);
-        var fileTransferStatuses = new List<FileTransferStatusEntity>();
-        await using (var reader = await command.ExecuteReaderAsync(cancellationToken))
+        
+        return await commandExecutor.ExecuteWithRetry(async (ct) =>
         {
-            while (await reader.ReadAsync(cancellationToken))
+            var fileTransferStatuses = new List<FileTransferStatusEntity>();
+            await using var reader = await command.ExecuteReaderAsync(ct);
+            while (await reader.ReadAsync(ct))
             {
                 fileTransferStatuses.Add(new FileTransferStatusEntity()
                 {
@@ -43,8 +47,8 @@ public async Task<List<FileTransferStatusEntity>> GetFileTransferStatusHistory(G
                     DetailedStatus = reader.IsDBNull(reader.GetOrdinal("file_transfer_status_detailed_description")) ? null : reader.GetString(reader.GetOrdinal("file_transfer_status_detailed_description"))
                 });
             }
-        }
-        return fileTransferStatuses;
+            return fileTransferStatuses;
+        }, cancellationToken);
     }
 
     public async Task<List<FileTransferStatusEntity>> GetCurrentFileTransferStatusesOfStatusAndOlderThanDate(FileTransferStatus statusFilter, DateTime minStatusDate, CancellationToken cancellationToken)
@@ -62,15 +66,14 @@ FROM broker.file_transfer_status
             )";
 
         await using var command = dataSource.CreateCommand(query);
-
         command.Parameters.AddWithValue("@statusFilter", (int)statusFilter);
         command.Parameters.AddWithValue("@minStatusDate", minStatusDate);
 
-        var fileTransferStatuses = new List<FileTransferStatusEntity>();
-
-        await using (var reader = await command.ExecuteReaderAsync(cancellationToken))
+        return await commandExecutor.ExecuteWithRetry(async (ct) =>
         {
-            while (await reader.ReadAsync(cancellationToken))
+            var fileTransferStatuses = new List<FileTransferStatusEntity>();
+            await using var reader = await command.ExecuteReaderAsync(ct);
+            while (await reader.ReadAsync(ct))
             {
                 fileTransferStatuses.Add(new FileTransferStatusEntity()
                 {
@@ -82,8 +85,7 @@ FROM broker.file_transfer_status
                         : reader.GetString(reader.GetOrdinal("file_transfer_status_detailed_description"))
                 });
             }
-        }
-
-        return fileTransferStatuses;
+            return fileTransferStatuses;
+        }, cancellationToken);
     }
 }
diff --git a/src/Altinn.Broker.Persistence/Repositories/IdempotencyEventRepository.cs b/src/Altinn.Broker.Persistence/Repositories/IdempotencyEventRepository.cs
index 5262845f..32ebc3c4 100644
--- a/src/Altinn.Broker.Persistence/Repositories/IdempotencyEventRepository.cs
+++ b/src/Altinn.Broker.Persistence/Repositories/IdempotencyEventRepository.cs
@@ -1,10 +1,11 @@
 ﻿using Altinn.Broker.Core.Repositories;
+using Altinn.Broker.Persistence.Helpers;
 
 using Npgsql;
 
 namespace Altinn.Broker.Persistence.Repositories;
 
-public class IdempotencyEventRepository(NpgsqlDataSource dataSource) : IIdempotencyEventRepository
+public class IdempotencyEventRepository(NpgsqlDataSource dataSource, ExecuteDBCommandWithRetries commandExecutor) : IIdempotencyEventRepository
 {
     public async Task AddIdempotencyEventAsync(string IdempotencyEventId, CancellationToken cancellationToken)
     {
@@ -14,6 +15,6 @@ public async Task AddIdempotencyEventAsync(string IdempotencyEventId, Cancellati
         command.Parameters.AddWithValue("@idempotency_event_id_pk", IdempotencyEventId);
         command.Parameters.AddWithValue("@created", DateTime.UtcNow);
 
-        await command.ExecuteNonQueryAsync(cancellationToken);
+        await commandExecutor.ExecuteWithRetry(command.ExecuteNonQueryAsync, cancellationToken);
     }
 }
diff --git a/src/Altinn.Broker.Persistence/Repositories/PartyRepository.cs b/src/Altinn.Broker.Persistence/Repositories/PartyRepository.cs
index 692c1a0d..01e1d009 100644
--- a/src/Altinn.Broker.Persistence/Repositories/PartyRepository.cs
+++ b/src/Altinn.Broker.Persistence/Repositories/PartyRepository.cs
@@ -1,10 +1,11 @@
 using Altinn.Broker.Core.Domain;
 using Altinn.Broker.Core.Repositories;
+using Altinn.Broker.Persistence.Helpers;
 
 using Npgsql;
 
 namespace Altinn.Broker.Persistence.Repositories;
-public class PartyRepository(NpgsqlDataSource dataSource) : IPartyRepository
+public class PartyRepository(NpgsqlDataSource dataSource, ExecuteDBCommandWithRetries commandExecutor) : IPartyRepository
 {
     public async Task<PartyEntity?> GetParty(string organizationId, CancellationToken cancellationToken)
     {
@@ -13,18 +14,21 @@ public class PartyRepository(NpgsqlDataSource dataSource) : IPartyRepository
             "WHERE organization_number_pk = @organizationId ");
         command.Parameters.AddWithValue("@organizationId", organizationId);
 
-        using NpgsqlDataReader reader = await command.ExecuteReaderAsync();
-        PartyEntity? partyData = null;
-        while (reader.Read())
+        return await commandExecutor.ExecuteWithRetry(async (ct) =>
         {
-            partyData = new PartyEntity
+            PartyEntity? partyData = null;
+            await using var reader = await command.ExecuteReaderAsync(ct);
+            while (await reader.ReadAsync(ct))
             {
-                OrganizationNumber = reader.GetString(reader.GetOrdinal("organization_number_pk")),
-                PartyId = reader.GetString(reader.GetOrdinal("party_id")),
-                Created = reader.GetDateTime(reader.GetOrdinal("created"))
-            };
-        }
-        return partyData;
+                partyData = new PartyEntity
+                {
+                    OrganizationNumber = reader.GetString(reader.GetOrdinal("organization_number_pk")),
+                    PartyId = reader.GetString(reader.GetOrdinal("party_id")),
+                    Created = reader.GetDateTime(reader.GetOrdinal("created"))
+                };
+            }
+            return partyData;
+        }, cancellationToken);
     }
 
     public async Task InitializeParty(string organizationId, string partyId)
@@ -34,8 +38,8 @@ public async Task InitializeParty(string organizationId, string partyId)
             "VALUES (@organizationId, @partyId, NOW())");
         command.Parameters.AddWithValue("@organizationId", organizationId);
         command.Parameters.AddWithValue("@partyId", partyId);
-        var commandText = command.CommandText;
-        command.ExecuteNonQuery();
+        
+        await commandExecutor.ExecuteWithRetry(command.ExecuteNonQueryAsync);
     }
 }
 
diff --git a/src/Altinn.Broker.Persistence/Repositories/ResourceRepository.cs b/src/Altinn.Broker.Persistence/Repositories/ResourceRepository.cs
index ab492b63..654baeaf 100644
--- a/src/Altinn.Broker.Persistence/Repositories/ResourceRepository.cs
+++ b/src/Altinn.Broker.Persistence/Repositories/ResourceRepository.cs
@@ -1,10 +1,11 @@
 using Altinn.Broker.Core.Domain;
 using Altinn.Broker.Core.Repositories;
+using Altinn.Broker.Persistence.Helpers;
 
 using Npgsql;
 
 namespace Altinn.Broker.Persistence.Repositories;
-public class ResourceRepository(NpgsqlDataSource dataSource, IAltinnResourceRepository altinnResourceRepository, IServiceOwnerRepository serviceOwnerRepository) : IResourceRepository
+public class ResourceRepository(NpgsqlDataSource dataSource, IAltinnResourceRepository altinnResourceRepository, IServiceOwnerRepository serviceOwnerRepository, ExecuteDBCommandWithRetries commandExecutor) : IResourceRepository
 {
     public async Task<ResourceEntity?> GetResource(string resourceId, CancellationToken cancellationToken)
     {
@@ -15,25 +16,29 @@ public class ResourceRepository(NpgsqlDataSource dataSource, IAltinnResourceRepo
             "ORDER BY created desc");
         command.Parameters.AddWithValue("@resourceId", resourceId);
 
-        using NpgsqlDataReader reader = await command.ExecuteReaderAsync();
         ResourceEntity? resource = null;
-        while (reader.Read())
+        
+        await using (var reader = await commandExecutor.ExecuteWithRetry(command.ExecuteReaderAsync, cancellationToken))
         {
-            resource = new ResourceEntity
+            while (await reader.ReadAsync(cancellationToken))
             {
-                Id = reader.GetString(reader.GetOrdinal("resource_id_pk")),
-                OrganizationNumber = reader.GetString(reader.GetOrdinal("organization_number")),
-                MaxFileTransferSize = reader.IsDBNull(reader.GetOrdinal("max_file_transfer_size")) ? null : reader.GetInt64(reader.GetOrdinal("max_file_transfer_size")),
-                FileTransferTimeToLive = reader.IsDBNull(reader.GetOrdinal("file_transfer_time_to_live")) ? null : reader.GetTimeSpan(reader.GetOrdinal("file_transfer_time_to_live")),
-                Created = reader.GetDateTime(reader.GetOrdinal("created")),
-                ServiceOwnerId = reader.GetString(reader.GetOrdinal("service_owner_id_fk")),
-                PurgeFileTransferAfterAllRecipientsConfirmed = reader.GetBoolean(reader.GetOrdinal("purge_file_transfer_after_all_recipients_confirmed")),
-                PurgeFileTransferGracePeriod = reader.IsDBNull(reader.GetOrdinal("purge_file_transfer_grace_period")) ? null : reader.GetTimeSpan(reader.GetOrdinal("purge_file_transfer_grace_period")),
-                UseManifestFileShim = reader.IsDBNull(reader.GetOrdinal("use_manifest_file_shim")) ? null : reader.GetBoolean(reader.GetOrdinal("use_manifest_file_shim")),
-                ExternalServiceCodeLegacy = reader.IsDBNull(reader.GetOrdinal("external_service_code_legacy")) ? null : reader.GetString(reader.GetOrdinal("external_service_code_legacy")),
-                ExternalServiceEditionCodeLegacy = reader.IsDBNull(reader.GetOrdinal("external_service_edition_code_legacy")) ? null : reader.GetInt32(reader.GetOrdinal("external_service_edition_code_legacy"))
-            };
+                resource = new ResourceEntity
+                {
+                    Id = reader.GetString(reader.GetOrdinal("resource_id_pk")),
+                    OrganizationNumber = reader.GetString(reader.GetOrdinal("organization_number")),
+                    MaxFileTransferSize = reader.IsDBNull(reader.GetOrdinal("max_file_transfer_size")) ? null : reader.GetInt64(reader.GetOrdinal("max_file_transfer_size")),
+                    FileTransferTimeToLive = reader.IsDBNull(reader.GetOrdinal("file_transfer_time_to_live")) ? null : reader.GetTimeSpan(reader.GetOrdinal("file_transfer_time_to_live")),
+                    Created = reader.GetDateTime(reader.GetOrdinal("created")),
+                    ServiceOwnerId = reader.GetString(reader.GetOrdinal("service_owner_id_fk")),
+                    PurgeFileTransferAfterAllRecipientsConfirmed = reader.GetBoolean(reader.GetOrdinal("purge_file_transfer_after_all_recipients_confirmed")),
+                    PurgeFileTransferGracePeriod = reader.IsDBNull(reader.GetOrdinal("purge_file_transfer_grace_period")) ? null : reader.GetTimeSpan(reader.GetOrdinal("purge_file_transfer_grace_period")),
+                    UseManifestFileShim = reader.IsDBNull(reader.GetOrdinal("use_manifest_file_shim")) ? null : reader.GetBoolean(reader.GetOrdinal("use_manifest_file_shim")),
+                    ExternalServiceCodeLegacy = reader.IsDBNull(reader.GetOrdinal("external_service_code_legacy")) ? null : reader.GetString(reader.GetOrdinal("external_service_code_legacy")),
+                    ExternalServiceEditionCodeLegacy = reader.IsDBNull(reader.GetOrdinal("external_service_edition_code_legacy")) ? null : reader.GetInt32(reader.GetOrdinal("external_service_edition_code_legacy"))
+                };
+            }
         }
+
         if (resource is null)
         {
             resource = await altinnResourceRepository.GetResource(resourceId, cancellationToken);
@@ -48,6 +53,7 @@ public class ResourceRepository(NpgsqlDataSource dataSource, IAltinnResourceRepo
         }
         return resource;
     }
+    
     public async Task CreateResource(ResourceEntity resource, CancellationToken cancellationToken)
     {
         await using var command = dataSource.CreateCommand(
@@ -58,8 +64,10 @@ public async Task CreateResource(ResourceEntity resource, CancellationToken canc
         command.Parameters.AddWithValue("@maxFileTransferSize", resource.MaxFileTransferSize == null ? DBNull.Value : resource.MaxFileTransferSize);
         command.Parameters.AddWithValue("@fileTransferTimeToLive", resource.FileTransferTimeToLive is null ? DBNull.Value : resource.FileTransferTimeToLive.Value);
         command.Parameters.AddWithValue("@serviceOwnerId", resource.ServiceOwnerId);
-        command.ExecuteNonQuery();
+        
+        await commandExecutor.ExecuteWithRetry(command.ExecuteNonQueryAsync, cancellationToken);
     }
+    
     public async Task UpdateMaxFileTransferSize(string resource, long maxSize, CancellationToken cancellationToken)
     {
         await using var command = dataSource.CreateCommand(
@@ -68,7 +76,8 @@ public async Task UpdateMaxFileTransferSize(string resource, long maxSize, Cance
             "WHERE resource_id_pk = @resourceId");
         command.Parameters.AddWithValue("@resourceId", resource);
         command.Parameters.AddWithValue("@maxFileTransferSize", maxSize);
-        command.ExecuteNonQuery();
+        
+        await commandExecutor.ExecuteWithRetry(command.ExecuteNonQueryAsync, cancellationToken);
     }
 
     public async Task UpdateFileRetention(string resourceId, TimeSpan fileTransferTimeToLive, CancellationToken cancellationToken = default)
@@ -79,8 +88,10 @@ public async Task UpdateFileRetention(string resourceId, TimeSpan fileTransferTi
             "WHERE resource_id_pk = @resourceId");
         command.Parameters.AddWithValue("@resourceId", resourceId);
         command.Parameters.AddWithValue("@fileTransferTimeToLive", fileTransferTimeToLive);
-        command.ExecuteNonQuery();
+        
+        await commandExecutor.ExecuteWithRetry(command.ExecuteNonQueryAsync, cancellationToken);
     }
+    
     public async Task UpdatePurgeFileTransferAfterAllRecipientsConfirmed(string resourceId, bool PurgeFileTransferAfterAllRecipientsConfirmed, CancellationToken cancellationToken = default)
     {
         await using var command = dataSource.CreateCommand(
@@ -89,8 +100,10 @@ public async Task UpdatePurgeFileTransferAfterAllRecipientsConfirmed(string reso
             "WHERE resource_id_pk = @resourceId");
         command.Parameters.AddWithValue("@resourceId", resourceId);
         command.Parameters.AddWithValue("@PurgeFileTransferAfterAllRecipientsConfirmed", PurgeFileTransferAfterAllRecipientsConfirmed);
-        command.ExecuteNonQuery();
+        
+        await commandExecutor.ExecuteWithRetry(command.ExecuteNonQueryAsync, cancellationToken);
     }
+
     public async Task UpdatePurgeFileTransferGracePeriod(string resourceId, TimeSpan PurgeFileTransferGracePeriod, CancellationToken cancellationToken = default)
     {
         await using var command = dataSource.CreateCommand(
@@ -99,7 +112,8 @@ public async Task UpdatePurgeFileTransferGracePeriod(string resourceId, TimeSpan
             "WHERE resource_id_pk = @resourceId");
         command.Parameters.AddWithValue("@resourceId", resourceId);
         command.Parameters.AddWithValue("@PurgeFileTransferGracePeriod", PurgeFileTransferGracePeriod);
-        command.ExecuteNonQuery();
+        
+        await commandExecutor.ExecuteWithRetry(command.ExecuteNonQueryAsync, cancellationToken);
     }
 
     public async Task UpdateUseManifestFileShim(string resourceId, bool useManifestFileShim, CancellationToken cancellationToken = default)
@@ -110,8 +124,10 @@ public async Task UpdateUseManifestFileShim(string resourceId, bool useManifestF
             "WHERE resource_id_pk = @resourceId");
         command.Parameters.AddWithValue("@resourceId", resourceId);
         command.Parameters.AddWithValue("@UseManifestFileShim", useManifestFileShim);
-        command.ExecuteNonQuery();
+        
+        await commandExecutor.ExecuteWithRetry(command.ExecuteNonQueryAsync, cancellationToken);
     }
+    
     public async Task UpdateExternalServiceCodeLegacy(string resourceId, string externalServiceCodeLegacy, CancellationToken cancellationToken = default)
     {
         await using var command = dataSource.CreateCommand(
@@ -120,8 +136,10 @@ public async Task UpdateExternalServiceCodeLegacy(string resourceId, string exte
             "WHERE resource_id_pk = @resourceId");
         command.Parameters.AddWithValue("@resourceId", resourceId);
         command.Parameters.AddWithValue("@externalServiceCodeLegacy", externalServiceCodeLegacy);
-        command.ExecuteNonQuery();
+        
+        await commandExecutor.ExecuteWithRetry(command.ExecuteNonQueryAsync, cancellationToken);
     }
+    
     public async Task UpdateExternalServiceEditionCodeLegacy(string resourceId, int? externalServiceEditionCodeLegacy, CancellationToken cancellationToken = default)
     {
         await using var command = dataSource.CreateCommand(
@@ -129,7 +147,8 @@ public async Task UpdateExternalServiceEditionCodeLegacy(string resourceId, int?
             "SET external_service_edition_code_legacy = @externalServiceEditionCodeLegacy " +
             "WHERE resource_id_pk = @resourceId");
         command.Parameters.AddWithValue("@resourceId", resourceId);
-                command.Parameters.AddWithValue("@externalServiceEditionCodeLegacy", (object?)externalServiceEditionCodeLegacy ?? DBNull.Value); 
-        command.ExecuteNonQuery();
+        command.Parameters.AddWithValue("@externalServiceEditionCodeLegacy", (object?)externalServiceEditionCodeLegacy ?? DBNull.Value);
+        
+        await commandExecutor.ExecuteWithRetry(command.ExecuteNonQueryAsync, cancellationToken);
     }
 }
diff --git a/src/Altinn.Broker.Persistence/Repositories/ServiceOwnerRepository.cs b/src/Altinn.Broker.Persistence/Repositories/ServiceOwnerRepository.cs
index 0bbe7ddd..27cfb004 100644
--- a/src/Altinn.Broker.Persistence/Repositories/ServiceOwnerRepository.cs
+++ b/src/Altinn.Broker.Persistence/Repositories/ServiceOwnerRepository.cs
@@ -1,10 +1,11 @@
 ﻿using Altinn.Broker.Core.Domain;
 using Altinn.Broker.Core.Repositories;
+using Altinn.Broker.Persistence.Helpers;
 
 using Npgsql;
 
 namespace Altinn.Broker.Persistence.Repositories;
-public class ServiceOwnerRepository(NpgsqlDataSource dataSource) : IServiceOwnerRepository
+public class ServiceOwnerRepository(NpgsqlDataSource dataSource, ExecuteDBCommandWithRetries commandExecutor) : IServiceOwnerRepository
 {
     public async Task<ServiceOwnerEntity?> GetServiceOwner(string serviceOwnerId)
     {
@@ -17,47 +18,50 @@ public class ServiceOwnerRepository(NpgsqlDataSource dataSource) : IServiceOwner
             "ORDER BY created desc");
         command.Parameters.AddWithValue("@serviceOwnerId", serviceOwnerId);
 
-        using NpgsqlDataReader reader = await command.ExecuteReaderAsync();
-        ServiceOwnerEntity? serviceOwner = null;
-        var storageProviders = new List<StorageProviderEntity>();
-
-        while (reader.Read())
+        return await commandExecutor.ExecuteWithRetry(async (ct) =>
         {
-            if (serviceOwner == null)
-            {
-                serviceOwner = new ServiceOwnerEntity
-                {
-                    Id = reader.GetString(reader.GetOrdinal("service_owner_id_pk")),
-                    Name = reader.GetString(reader.GetOrdinal("service_owner_name")),
-                    StorageProviders = new List<StorageProviderEntity>()
-                };
-            }
+            ServiceOwnerEntity? serviceOwner = null;
+            var storageProviders = new List<StorageProviderEntity>();
 
-            if (!reader.IsDBNull(reader.GetOrdinal("storage_provider_id_pk")))
+            await using var reader = await command.ExecuteReaderAsync(ct);
+            while (reader.Read())
             {
-                var storageProvider = new StorageProviderEntity
+                if (serviceOwner == null)
                 {
-                    ServiceOwnerId = serviceOwnerId,
-                    Created = reader.GetDateTime(reader.GetOrdinal("created")),
-                    Id = reader.GetInt64(reader.GetOrdinal("storage_provider_id_pk")),
-                    ResourceName = reader.GetString(reader.GetOrdinal("resource_name")),
-                    Type = Enum.Parse<StorageProviderType>(reader.GetString(reader.GetOrdinal("storage_provider_type"))),
-                    Active = reader.GetBoolean(reader.GetOrdinal("active"))
-                };
+                    serviceOwner = new ServiceOwnerEntity
+                    {
+                        Id = reader.GetString(reader.GetOrdinal("service_owner_id_pk")),
+                        Name = reader.GetString(reader.GetOrdinal("service_owner_name")),
+                        StorageProviders = new List<StorageProviderEntity>()
+                    };
+                }
 
-                if (storageProvider.Active && !storageProviders.Any(sp => sp.Id == storageProvider.Id))
+                if (!reader.IsDBNull(reader.GetOrdinal("storage_provider_id_pk")))
                 {
-                    storageProviders.Add(storageProvider);
+                    var storageProvider = new StorageProviderEntity
+                    {
+                        ServiceOwnerId = serviceOwnerId,
+                        Created = reader.GetDateTime(reader.GetOrdinal("created")),
+                        Id = reader.GetInt64(reader.GetOrdinal("storage_provider_id_pk")),
+                        ResourceName = reader.GetString(reader.GetOrdinal("resource_name")),
+                        Type = Enum.Parse<StorageProviderType>(reader.GetString(reader.GetOrdinal("storage_provider_type"))),
+                        Active = reader.GetBoolean(reader.GetOrdinal("active"))
+                    };
+
+                    if (!storageProviders.Any(sp => sp.Id == storageProvider.Id))
+                    {
+                        storageProviders.Add(storageProvider);
+                    }
                 }
             }
-        }
 
-        if (serviceOwner != null)
-        {
-            serviceOwner.StorageProviders = storageProviders;
-        }
+            if (serviceOwner != null)
+            {
+                serviceOwner.StorageProviders = storageProviders;
+            }
 
-        return serviceOwner;
+            return serviceOwner;
+        });
     }
 
     public async Task InitializeServiceOwner(string sub, string name)
@@ -67,8 +71,8 @@ public async Task InitializeServiceOwner(string sub, string name)
             "VALUES (@sub, @name)");
         command.Parameters.AddWithValue("@sub", sub);
         command.Parameters.AddWithValue("@name", name);
-        var commandText = command.CommandText;
-        command.ExecuteNonQuery();
+        
+        await commandExecutor.ExecuteWithRetry(command.ExecuteNonQueryAsync);
     }
 
     public async Task InitializeStorageProvider(string sub, string resourceName, StorageProviderType storageType)
@@ -79,7 +83,8 @@ public async Task InitializeStorageProvider(string sub, string resourceName, Sto
         command.Parameters.AddWithValue("@resourceName", resourceName);
         command.Parameters.AddWithValue("@storageType", storageType.ToString());
         command.Parameters.AddWithValue("@serviceOwnerId", sub);
-        command.ExecuteNonQuery();
+        
+        await commandExecutor.ExecuteWithRetry(command.ExecuteNonQueryAsync);
     }
 }
 


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 4**

<details>
<summary>🔭 Outside diff range comments (3)</summary><blockquote>

<details>
<summary>src/Altinn.Broker.Persistence/Repositories/FileTransferRepository.cs (2)</summary><blockquote>

`455-505`: _⚠️ Potential issue_

**Column-name mismatch between SELECT and INSERT for metadata**

`GetMetadata` queries `property_name / property_value`, whereas `SetMetadata` inserts into columns named `key / value`:

```sql
SELECT property_name, property_value FROM broker.file_transfer_property …
INSERT INTO broker.file_transfer_property (file_transfer_id_fk, key, value) …
```

If the table really has been renamed to `property_name / property_value`, the insert will now fail with  
`column "key" does not exist` (or vice-versa), silently breaking metadata persistence.

Please align the column names in both methods (and in the DB schema) to avoid runtime errors.

---

`20-74`: _💡 Verification agent_

_🛠️ Refactor suggestion_

<details>
<summary>❓ Verification inconclusive</summary>

**Potentially brittle: the same `NpgsqlCommand` instance is reused across retries**

`command` is instantiated *outside* the `ExecuteWithRetry` delegate and is therefore reused if Polly retries the operation.  
If the first execution fails after opening a connection/reader, the command may hold a broken or busy connection, causing all subsequent retry-attempts to fail immediately (or even leak a connection).

A safer pattern is to build the `NpgsqlCommand` *inside* the retry delegate so that every retry gets a brand-new command and connection:

```diff
- await using var command = dataSource.CreateCommand(@" … ");
- …                              
- command.Parameters.AddWithValue("@fileTransferId", fileTransferId);
-
- return await commandExecutor.ExecuteWithRetry(async (ct) =>
- {
-     FileTransferEntity? fileTransfer = null;
-     await using var reader = await command.ExecuteReaderAsync(ct);
+ return await commandExecutor.ExecuteWithRetry(async (ct) =>
+ {
+     await using var command = dataSource.CreateCommand(@" … ");
+     command.Parameters.AddWithValue("@fileTransferId", fileTransferId);
+
+     FileTransferEntity? fileTransfer = null;
+     await using var reader = await command.ExecuteReaderAsync(ct);
      …
 }, cancellationToken);
```

The same refactor should be applied to other methods that pass a *pre-created* `command` instance into `ExecuteWithRetry`.

---

#!/bin/bash
# Locate all usages of ExecuteWithRetry
rg -n "return await commandExecutor.ExecuteWithRetry" -n .

# Locate all instances of commands created outside retry delegates
rg -n "dataSource.CreateCommand" -n .

---



</details>

**Instantiate NpgsqlCommand inside each retry to avoid stale/broken connections**

Reusing a single `NpgsqlCommand` instance across Polly retry attempts can cause subsequent retries to operate on a broken or busy connection, leading to immediate failures or connection leaks. Move the command creation into the `ExecuteWithRetry` delegate so that each attempt gets a fresh command and connection.

Affected locations:
- `src/Altinn.Broker.Persistence/Repositories/FileTransferRepository.cs` (lines 20–74)
- Any other methods where `dataSource.CreateCommand` is invoked outside the retry delegate before calling `ExecuteWithRetry`

Proposed diff:
```diff
- await using var command = dataSource.CreateCommand(@"…");
- command.Parameters.AddWithValue("@fileTransferId", fileTransferId);
-
- return await commandExecutor.ExecuteWithRetry(async ct =>
- {
-     FileTransferEntity? fileTransfer = null;
-     await using var reader = await command.ExecuteReaderAsync(ct);
+ return await commandExecutor.ExecuteWithRetry(async ct =>
+ {
+     await using var command = dataSource.CreateCommand(@"…");
+     command.Parameters.AddWithValue("@fileTransferId", fileTransferId);
+
+     FileTransferEntity? fileTransfer = null;
+     await using var reader = await command.ExecuteReaderAsync(ct);
     …
 }, cancellationToken);
```

</blockquote></details>
<details>
<summary>src/Altinn.Broker.Persistence/Repositories/ResourceRepository.cs (1)</summary><blockquote>

`12-24`: _🛠️ Refactor suggestion_

**Same command-reuse issue can break retries**

`command` is constructed once, then its `ExecuteReaderAsync` method group is passed to `ExecuteWithRetry`.  
Should Polly retry, the same `NpgsqlCommand` (and underlying connection) is reused, suffering from the same hazards described for `FileTransferRepository`.

Refactor by moving command creation inside the retry delegate:

```diff
- await using var command = dataSource.CreateCommand("SELECT …");
- command.Parameters.AddWithValue("@resourceId", resourceId);
-
- await using (var reader = await commandExecutor.ExecuteWithRetry(command.ExecuteReaderAsync, cancellationToken))
+ await using (var reader = await commandExecutor.ExecuteWithRetry(
+     async ct =>
+     {
+         await using var cmd = dataSource.CreateCommand("SELECT …");
+         cmd.Parameters.AddWithValue("@resourceId", resourceId);
+         return await cmd.ExecuteReaderAsync(ct);
+     }, cancellationToken))
```

Apply the same pattern to **all** `Update*/Create*` methods below to guarantee a fresh command/connection per retry.

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (7)</summary><blockquote>

<details>
<summary>src/Altinn.Broker.Persistence/Helpers/ExecuteDBCommandWithRetries.cs (4)</summary><blockquote>

`56-71`: **Consider using exponential backoff instead of fixed delay**

The retry policy uses a fixed delay of 100ms between retry attempts. For better handling of contention and transient issues that might need more time to resolve, consider implementing exponential backoff similar to what's used in `TransactionWithRetriesPolicy.RetryPolicy()`.


```diff
return Policy
    .Handle<NpgsqlException>(ex => ex.IsTransient)
    .Or<PostgresException>()
    .WaitAndRetryAsync(
        3,
-       retryAttempt => TimeSpan.FromMilliseconds(100),
+       retryAttempt => TimeSpan.FromMilliseconds(Math.Min(100 * Math.Pow(2, retryAttempt - 1), 400)),
        (exception, timeSpan, retryCount, context) =>
        {
            _logger.LogWarning("Database command attempt {RetryCount} failed with exception: {ExceptionMessage}. Retrying in {RetryTimeSpan}ms...",
                retryCount,
                exception.Message,
                timeSpan.TotalMilliseconds);
        });
```

---

`11-72`: **Consider making retry configuration injectable**

The retry count (3) and delay (100ms) are hardcoded in the class. Consider making these configurable through options, allowing different environments to adjust retry behavior without code changes.


Create a configuration class and use it in the constructor:

```csharp
public class RetryPolicyOptions
{
    public int RetryCount { get; set; } = 3;
    public int BaseDelayMilliseconds { get; set; } = 100;
    public bool UseExponentialBackoff { get; set; } = true;
    public int MaxDelayMilliseconds { get; set; } = 400;
}

public class ExecuteDBCommandWithRetries
{
    private readonly ILogger<ExecuteDBCommandWithRetries> _logger;
    private readonly RetryPolicyOptions _options;

    public ExecuteDBCommandWithRetries(
        ILogger<ExecuteDBCommandWithRetries> logger, 
        RetryPolicyOptions options)
    {
        _logger = logger;
        _options = options;
    }
    
    // ... rest of the class
    
    private AsyncRetryPolicy CreateRetryPolicy()
    {
        return Policy
            .Handle<NpgsqlException>(ex => ex.IsTransient)
            .Or<PostgresException>()
            .WaitAndRetryAsync(
                _options.RetryCount,
                retryAttempt => _options.UseExponentialBackoff 
                    ? TimeSpan.FromMilliseconds(Math.Min(_options.BaseDelayMilliseconds * Math.Pow(2, retryAttempt - 1), _options.MaxDelayMilliseconds))
                    : TimeSpan.FromMilliseconds(_options.BaseDelayMilliseconds),
                // ... rest of the method
            );
    }
}
```

Then register it in DependencyInjection.cs:

```csharp
services.AddSingleton(new RetryPolicyOptions());
services.AddSingleton<ExecuteDBCommandWithRetries>();
```

---

`22-35`: **Consider handling specific errors differently**

The current implementation treats all transient errors the same way. In some cases, you might want to immediately fail on certain errors or apply different retry strategies based on the specific exception.


You could enhance the policy to handle specific PostgreSQL error codes differently:

```csharp
public async Task<T> ExecuteWithRetry<T>(Func<CancellationToken, Task<T>> operation, CancellationToken cancellationToken = default)
{
    var result = await CreateRetryPolicy().ExecuteAndCaptureAsync(operation, cancellationToken);
    
    if (result.Outcome == OutcomeType.Failure)
    {
        // Specific handling for certain PostgreSQL errors
        if (result.FinalException is PostgresException pgEx)
        {
            // Example: connection timeout might need a different message or handling
            if (pgEx.SqlState == "57014") // query_canceled
            {
                _logger.LogError("Query was canceled during execution: {message}", pgEx.Message);
                throw new OperationCanceledException("Database query was canceled", pgEx);
            }
        }
        
        _logger.LogError("Exception during database command retries: {message}\n{stackTrace}", 
            result.FinalException.Message, 
            result.FinalException.StackTrace);
        throw result.FinalException;
    }
    
    return result.Result;
}
```

---

`58-61`: **Add handling for additional relevant exception types**

The current implementation only handles `NpgsqlException` (when transient) and `PostgresException`. Consider handling additional exception types that might occur during database operations, especially network-related exceptions.


```diff
return Policy
    .Handle<NpgsqlException>(ex => ex.IsTransient)
    .Or<PostgresException>()
+   .Or<TimeoutException>()
+   .Or<System.Net.Sockets.SocketException>()
+   .Or<System.IO.IOException>()
    .WaitAndRetryAsync(
```

</blockquote></details>
<details>
<summary>src/Altinn.Broker.Persistence/Repositories/ActorRepository.cs (1)</summary><blockquote>

`41-43`: **Remove null-forgiving operator with proper null checking**

The use of the null-forgiving operator (`!`) on line 42 assumes that `ExecuteScalarAsync` will never return null, which might not always be true.


```diff
return await commandExecutor.ExecuteWithRetry(async (ct) => 
-   (long)(await command.ExecuteScalarAsync(ct))!, 
+   {
+       var result = await command.ExecuteScalarAsync(ct);
+       if (result is null)
+       {
+           throw new InvalidOperationException("Failed to retrieve actor ID after insertion");
+       }
+       return (long)result;
+   }, 
    cancellationToken);
```

</blockquote></details>
<details>
<summary>src/Altinn.Broker.Persistence/Repositories/FileTransferRepository.cs (1)</summary><blockquote>

`521-532`: **Redundant null-check on non-nullable `hangfireJobId`**

`hangfireJobId` is declared as `string` (non-nullable), yet later treated as potentially `null`.

```csharp
command.Parameters.AddWithValue("@hangfireJobId", hangfireJobId is null ? DBNull.Value : hangfireJobId);
```

Either:

1. Mark the parameter as nullable (`string? hangfireJobId`) if callers are allowed to pass `null`, or  
2. Remove the null-check and pass `hangfireJobId` directly.

This keeps null-state analysis consistent and avoids dead code.

</blockquote></details>
<details>
<summary>src/Altinn.Broker.Persistence/Repositories/ResourceRepository.cs (1)</summary><blockquote>

`59-69`: **Missing cancellation token in `ExecuteNonQueryAsync` when wrapped**

`ExecuteNonQueryAsync` receives an optional `CancellationToken`, but in `CreateResource` (and other update methods) you pass the method group directly:

```csharp
await commandExecutor.ExecuteWithRetry(command.ExecuteNonQueryAsync, cancellationToken);
```

Because of the optional parameter, this compiles, but `command.ExecuteNonQueryAsync` will be invoked *without* the supplied `cancellationToken`.  
Wrap the call in a lambda to forward the token explicitly:

```diff
- await commandExecutor.ExecuteWithRetry(command.ExecuteNonQueryAsync, cancellationToken);
+ await commandExecutor.ExecuteWithRetry(ct => command.ExecuteNonQueryAsync(ct), cancellationToken);
```

This ensures that long-running DB writes respect request cancellation.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 619f8eeb1a38761d7969017e7977c7657778c60a and 1712380ddee4f5259d5a3a956b53379f72861197.

</details>

<details>
<summary>📒 Files selected for processing (10)</summary>

* `src/Altinn.Broker.Persistence/DependencyInjection.cs` (2 hunks)
* `src/Altinn.Broker.Persistence/Helpers/ExecuteDBCommandWithRetries.cs` (1 hunks)
* `src/Altinn.Broker.Persistence/Repositories/ActorFileTransferStatusRepository.cs` (4 hunks)
* `src/Altinn.Broker.Persistence/Repositories/ActorRepository.cs` (2 hunks)
* `src/Altinn.Broker.Persistence/Repositories/FileTransferRepository.cs` (14 hunks)
* `src/Altinn.Broker.Persistence/Repositories/FileTransferStatusRepository.cs` (6 hunks)
* `src/Altinn.Broker.Persistence/Repositories/IdempotencyEventRepository.cs` (2 hunks)
* `src/Altinn.Broker.Persistence/Repositories/PartyRepository.cs` (3 hunks)
* `src/Altinn.Broker.Persistence/Repositories/ResourceRepository.cs` (10 hunks)
* `src/Altinn.Broker.Persistence/Repositories/ServiceOwnerRepository.cs` (4 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (6)</summary>

<details>
<summary>src/Altinn.Broker.Persistence/Repositories/FileTransferStatusRepository.cs (2)</summary><blockquote>

<details>
<summary>src/Altinn.Broker.Persistence/Helpers/ExecuteDBCommandWithRetries.cs (1)</summary>

* `ExecuteDBCommandWithRetries` (11-72)

</details>
<details>
<summary>src/Altinn.Broker.Core/Domain/FileTransferStatusEntity.cs (1)</summary>

* `FileTransferStatusEntity` (2-8)

</details>

</blockquote></details>
<details>
<summary>src/Altinn.Broker.Persistence/Repositories/IdempotencyEventRepository.cs (2)</summary><blockquote>

<details>
<summary>src/Altinn.Broker.Persistence/DependencyInjection.cs (1)</summary>

* `NpgsqlDataSource` (28-54)

</details>
<details>
<summary>src/Altinn.Broker.Persistence/Helpers/ExecuteDBCommandWithRetries.cs (1)</summary>

* `ExecuteDBCommandWithRetries` (11-72)

</details>

</blockquote></details>
<details>
<summary>src/Altinn.Broker.Persistence/DependencyInjection.cs (1)</summary><blockquote>

<details>
<summary>src/Altinn.Broker.Persistence/Helpers/ExecuteDBCommandWithRetries.cs (1)</summary>

* `ExecuteDBCommandWithRetries` (11-72)

</details>

</blockquote></details>
<details>
<summary>src/Altinn.Broker.Persistence/Repositories/PartyRepository.cs (2)</summary><blockquote>

<details>
<summary>src/Altinn.Broker.Persistence/Helpers/ExecuteDBCommandWithRetries.cs (1)</summary>

* `ExecuteDBCommandWithRetries` (11-72)

</details>
<details>
<summary>src/Altinn.Broker.Core/Domain/PartyEntity.cs (1)</summary>

* `PartyEntity` (3-8)

</details>

</blockquote></details>
<details>
<summary>src/Altinn.Broker.Persistence/Helpers/ExecuteDBCommandWithRetries.cs (1)</summary><blockquote>

<details>
<summary>src/Altinn.Broker.Core/Helpers/TransactionWithRetriesPolicy.cs (2)</summary>

* `Task` (16-34)
* `AsyncRetryPolicy` (36-48)

</details>

</blockquote></details>
<details>
<summary>src/Altinn.Broker.Persistence/Repositories/ServiceOwnerRepository.cs (3)</summary><blockquote>

<details>
<summary>src/Altinn.Broker.Persistence/Helpers/ExecuteDBCommandWithRetries.cs (1)</summary>

* `ExecuteDBCommandWithRetries` (11-72)

</details>
<details>
<summary>src/Altinn.Broker.Core/Domain/ServiceOwnerEntity.cs (2)</summary>

* `ServiceOwnerEntity` (5-22)
* `StorageProviderEntity` (11-21)

</details>
<details>
<summary>src/Altinn.Broker.Core/Domain/StorageProviderEntity.cs (1)</summary>

* `StorageProviderEntity` (1-11)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (4)</summary>

* GitHub Check: Deploy
* GitHub Check: Test application
* GitHub Check: Analyze (csharp)
* GitHub Check: Analyze (javascript-typescript)

</details>

<details>
<summary>🔇 Additional comments (19)</summary><blockquote>

<details>
<summary>src/Altinn.Broker.Persistence/DependencyInjection.cs (1)</summary>

`20-20`: **Implementation looks good**

The registration of `ExecuteDBCommandWithRetries` as a singleton service is appropriate since the class is stateless and can be safely shared across multiple repository instances.

</details>
<details>
<summary>src/Altinn.Broker.Persistence/Repositories/IdempotencyEventRepository.cs (2)</summary>

`8-8`: **Implementation looks good**

The constructor properly accepts the new dependency and uses C# primary constructor syntax.

---

`18-18`: **Implementation looks good**

The method `ExecuteWithRetry` is correctly used here to wrap the database command execution with retry logic. The command delegate and cancellation token are properly passed.

</details>
<details>
<summary>src/Altinn.Broker.Persistence/Repositories/ActorRepository.cs (2)</summary>

`9-9`: **Implementation looks good**

The constructor properly accepts the new dependency and uses C# primary constructor syntax.

---

`17-30`: **Implementation looks good, but handle null return value explicitly**

The retry logic correctly wraps the database command execution including the reader processing. The cancellation token is properly passed through all layers.

</details>
<details>
<summary>src/Altinn.Broker.Persistence/Repositories/ActorFileTransferStatusRepository.cs (4)</summary>

`4-4`: **Good addition of the Helpers namespace import**

The import is correctly added to enable the use of the `ExecuteDBCommandWithRetries` helper class.

---

`9-9`: **Constructor injection properly implemented**

The `ExecuteDBCommandWithRetries` dependency is correctly injected via the primary constructor pattern.

---

`19-39`: **Well-implemented retry mechanism for ExecuteReaderAsync**

The database operation is properly wrapped in a retry mechanism while maintaining the original functionality. The cancellation token is correctly propagated to both the retry mechanism and the database operations.

---

`57-65`: **Appropriate retry logic for ExecuteNonQueryAsync**

The non-query command execution is correctly wrapped with the retry mechanism, and the cancellation token is properly forwarded.

</details>
<details>
<summary>src/Altinn.Broker.Persistence/Repositories/FileTransferStatusRepository.cs (5)</summary>

`4-4`: **Good addition of the Helpers namespace import**

The import is correctly added to enable the use of the `ExecuteDBCommandWithRetries` helper class.

---

`9-9`: **Constructor injection properly implemented**

The `ExecuteDBCommandWithRetries` dependency is correctly injected via the primary constructor pattern.

---

`20-20`: **Well-implemented retry mechanism for ExecuteScalarAsync**

The scalar command execution is properly wrapped in a retry mechanism with the cancellation token correctly propagated.

---

`36-51`: **Well-implemented retry mechanism for GetFileTransferStatusHistory**

The database read operation is properly wrapped in a retry mechanism while maintaining the original functionality. The cancellation token is correctly propagated to both the retry mechanism and the database operations.

---

`72-89`: **Well-implemented retry mechanism for GetCurrentFileTransferStatusesOfStatusAndOlderThanDate**

The database read operation is properly wrapped in a retry mechanism while maintaining the original functionality. The cancellation token is correctly propagated to both the retry mechanism and the database operations.

</details>
<details>
<summary>src/Altinn.Broker.Persistence/Repositories/PartyRepository.cs (3)</summary>

`3-3`: **Good addition of the Helpers namespace import**

The import is correctly added to enable the use of the `ExecuteDBCommandWithRetries` helper class.

---

`8-8`: **Constructor injection properly implemented**

The `ExecuteDBCommandWithRetries` dependency is correctly injected via the primary constructor pattern.

---

`17-31`: **Well-implemented retry mechanism for GetParty**

The database read operation is properly wrapped in a retry mechanism while maintaining the original functionality. The cancellation token is correctly propagated to both the retry mechanism and the database operations.

</details>
<details>
<summary>src/Altinn.Broker.Persistence/Repositories/ServiceOwnerRepository.cs (2)</summary>

`3-3`: **Good addition of the Helpers namespace import**

The import is correctly added to enable the use of the `ExecuteDBCommandWithRetries` helper class.

---

`8-8`: **Constructor injection properly implemented**

The `ExecuteDBCommandWithRetries` dependency is correctly injected via the primary constructor pattern.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T13:24:41Z
user: login: Ceredron
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-13T08:40:41Z
user: login: Ceredron
body: Great reflections!
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-14T06:22:58Z

Review comments:
diff_hunk: @@ -34,8 +38,8 @@ public async Task InitializeParty(string organizationId, string partyId)
             "VALUES (@organizationId, @partyId, NOW())");
         command.Parameters.AddWithValue("@organizationId", organizationId);
         command.Parameters.AddWithValue("@partyId", partyId);
-        var commandText = command.CommandText;
-        command.ExecuteNonQuery();
+        
+        await commandExecutor.ExecuteWithRetry(command.ExecuteNonQueryAsync);
path: src/Altinn.Broker.Persistence/Repositories/PartyRepository.cs
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Missing cancellation token in InitializeParty method**

Unlike other methods in this repository and other repositories, the `InitializeParty` method doesn't pass a cancellation token to the retry executor. Consider adding a cancellation token parameter to maintain consistency and allow for proper cancellation.

```diff
-public async Task InitializeParty(string organizationId, string partyId)
+public async Task InitializeParty(string organizationId, string partyId, CancellationToken cancellationToken = default)
 {
     await using var command = dataSource.CreateCommand(
         "INSERT INTO broker.party (organization_number_pk, party_id, created) " +
         "VALUES (@organizationId, @partyId, NOW())");
     command.Parameters.AddWithValue("@organizationId", organizationId);
     command.Parameters.AddWithValue("@partyId", partyId);
     
-    await commandExecutor.ExecuteWithRetry(command.ExecuteNonQueryAsync);
+    await commandExecutor.ExecuteWithRetry(command.ExecuteNonQueryAsync, cancellationToken);
 }
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
 public async Task InitializeParty(string organizationId, string partyId, CancellationToken cancellationToken = default)
 {
     await using var command = dataSource.CreateCommand(
         "INSERT INTO broker.party (organization_number_pk, party_id, created) " +
         "VALUES (@organizationId, @partyId, NOW())");
     command.Parameters.AddWithValue("@organizationId", organizationId);
     command.Parameters.AddWithValue("@partyId", partyId);
     
-    await commandExecutor.ExecuteWithRetry(command.ExecuteNonQueryAsync);
+    await commandExecutor.ExecuteWithRetry(command.ExecuteNonQueryAsync, cancellationToken);
 }
`````

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents (early access)</summary>

```
In src/Altinn.Broker.Persistence/Repositories/PartyRepository.cs around lines 41
to 42, the InitializeParty method calls ExecuteWithRetry without passing a
cancellation token, unlike other methods in this repository. Modify the
InitializeParty method signature to accept a CancellationToken parameter and
pass this token to the ExecuteWithRetry call to ensure consistent cancellation
support across methods.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-12T13:24:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -67,8 +71,8 @@ public async Task InitializeServiceOwner(string sub, string name)
             "VALUES (@sub, @name)");
         command.Parameters.AddWithValue("@sub", sub);
         command.Parameters.AddWithValue("@name", name);
-        var commandText = command.CommandText;
-        command.ExecuteNonQuery();
+        
+        await commandExecutor.ExecuteWithRetry(command.ExecuteNonQueryAsync);
path: src/Altinn.Broker.Persistence/Repositories/ServiceOwnerRepository.cs
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Missing cancellation token parameter**

Unlike other repositories, the `InitializeServiceOwner` method doesn't include a cancellation token parameter. Consider adding one to maintain consistency across all repositories and enable proper cancellation of operations.


```diff
-public async Task InitializeServiceOwner(string sub, string name)
+public async Task InitializeServiceOwner(string sub, string name, CancellationToken cancellationToken = default)
 {
     await using var command = dataSource.CreateCommand(
         "INSERT INTO broker.service_owner (service_owner_id_pk, service_owner_name) " +
         "VALUES (@sub, @name)");
     command.Parameters.AddWithValue("@sub", sub);
     command.Parameters.AddWithValue("@name", name);
     
-    await commandExecutor.ExecuteWithRetry(command.ExecuteNonQueryAsync);
+    await commandExecutor.ExecuteWithRetry(command.ExecuteNonQueryAsync, cancellationToken);
 }
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
public async Task InitializeServiceOwner(string sub, string name, CancellationToken cancellationToken = default)
{
    await using var command = dataSource.CreateCommand(
        "INSERT INTO broker.service_owner (service_owner_id_pk, service_owner_name) " +
        "VALUES (@sub, @name)");
    command.Parameters.AddWithValue("@sub", sub);
    command.Parameters.AddWithValue("@name", name);
    
    await commandExecutor.ExecuteWithRetry(command.ExecuteNonQueryAsync, cancellationToken);
}
`````

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents (early access)</summary>

```
In src/Altinn.Broker.Persistence/Repositories/ServiceOwnerRepository.cs around
lines 74 to 75, the InitializeServiceOwner method is missing a cancellation
token parameter, unlike other repositories. Add a CancellationToken parameter to
the method signature and pass it to the ExecuteNonQueryAsync call via the
commandExecutor.ExecuteWithRetry method to enable proper cancellation and
maintain consistency.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-12T13:24:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -79,7 +83,8 @@
         command.Parameters.AddWithValue("@resourceName", resourceName);
         command.Parameters.AddWithValue("@storageType", storageType.ToString());
         command.Parameters.AddWithValue("@serviceOwnerId", sub);
-        command.ExecuteNonQuery();
+        
+        await commandExecutor.ExecuteWithRetry(command.ExecuteNonQueryAsync);
path: src/Altinn.Broker.Persistence/Repositories/ServiceOwnerRepository.cs
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Missing cancellation token parameter**

Similar to the `InitializeServiceOwner` method, the `InitializeStorageProvider` method is missing a cancellation token parameter. Add one to maintain consistency with other repositories.


```diff
-public async Task InitializeStorageProvider(string sub, string resourceName, StorageProviderType storageType)
+public async Task InitializeStorageProvider(string sub, string resourceName, StorageProviderType storageType, CancellationToken cancellationToken = default)
 {
     await using var command = dataSource.CreateCommand(
         "INSERT INTO broker.storage_provider (created, resource_name, storage_provider_type, service_owner_id_fk) " +
         "VALUES (NOW(), @resourceName, @storageType, @serviceOwnerId)");
     command.Parameters.AddWithValue("@resourceName", resourceName);
     command.Parameters.AddWithValue("@storageType", storageType.ToString());
     command.Parameters.AddWithValue("@serviceOwnerId", sub);
     
-    await commandExecutor.ExecuteWithRetry(command.ExecuteNonQueryAsync);
+    await commandExecutor.ExecuteWithRetry(command.ExecuteNonQueryAsync, cancellationToken);
 }
```

<details>
<summary>🤖 Prompt for AI Agents (early access)</summary>

```
In src/Altinn.Broker.Persistence/Repositories/ServiceOwnerRepository.cs around
lines 86 to 87, the InitializeStorageProvider method is missing a cancellation
token parameter, unlike the InitializeServiceOwner method. Add a cancellation
token parameter to the InitializeStorageProvider method signature and pass it to
the ExecuteNonQueryAsync call within ExecuteWithRetry to maintain consistency
and support cancellation.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-12T13:24:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -17,47 +18,50 @@
             "ORDER BY created desc");
         command.Parameters.AddWithValue("@serviceOwnerId", serviceOwnerId);
 
-        using NpgsqlDataReader reader = await command.ExecuteReaderAsync();
-        ServiceOwnerEntity? serviceOwner = null;
-        var storageProviders = new List<StorageProviderEntity>();
-
-        while (reader.Read())
+        return await commandExecutor.ExecuteWithRetry(async (ct) =>
         {
-            if (serviceOwner == null)
-            {
-                serviceOwner = new ServiceOwnerEntity
-                {
-                    Id = reader.GetString(reader.GetOrdinal("service_owner_id_pk")),
-                    Name = reader.GetString(reader.GetOrdinal("service_owner_name")),
-                    StorageProviders = new List<StorageProviderEntity>()
-                };
-            }
+            ServiceOwnerEntity? serviceOwner = null;
+            var storageProviders = new List<StorageProviderEntity>();
 
-            if (!reader.IsDBNull(reader.GetOrdinal("storage_provider_id_pk")))
+            await using var reader = await command.ExecuteReaderAsync(ct);
+            while (reader.Read())
             {
-                var storageProvider = new StorageProviderEntity
+                if (serviceOwner == null)
                 {
-                    ServiceOwnerId = serviceOwnerId,
-                    Created = reader.GetDateTime(reader.GetOrdinal("created")),
-                    Id = reader.GetInt64(reader.GetOrdinal("storage_provider_id_pk")),
-                    ResourceName = reader.GetString(reader.GetOrdinal("resource_name")),
-                    Type = Enum.Parse<StorageProviderType>(reader.GetString(reader.GetOrdinal("storage_provider_type"))),
-                    Active = reader.GetBoolean(reader.GetOrdinal("active"))
-                };
+                    serviceOwner = new ServiceOwnerEntity
+                    {
+                        Id = reader.GetString(reader.GetOrdinal("service_owner_id_pk")),
+                        Name = reader.GetString(reader.GetOrdinal("service_owner_name")),
+                        StorageProviders = new List<StorageProviderEntity>()
+                    };
+                }
 
-                if (storageProvider.Active && !storageProviders.Any(sp => sp.Id == storageProvider.Id))
+                if (!reader.IsDBNull(reader.GetOrdinal("storage_provider_id_pk")))
                 {
-                    storageProviders.Add(storageProvider);
+                    var storageProvider = new StorageProviderEntity
+                    {
+                        ServiceOwnerId = serviceOwnerId,
+                        Created = reader.GetDateTime(reader.GetOrdinal("created")),
+                        Id = reader.GetInt64(reader.GetOrdinal("storage_provider_id_pk")),
+                        ResourceName = reader.GetString(reader.GetOrdinal("resource_name")),
+                        Type = Enum.Parse<StorageProviderType>(reader.GetString(reader.GetOrdinal("storage_provider_type"))),
+                        Active = reader.GetBoolean(reader.GetOrdinal("active"))
+                    };
+
+                    if (storageProvider.Active && !storageProviders.Any(sp => sp.Id == storageProvider.Id))
+                    {
+                        storageProviders.Add(storageProvider);
+                    }
                 }
             }
-        }
 
-        if (serviceOwner != null)
-        {
-            serviceOwner.StorageProviders = storageProviders;
-        }
+            if (serviceOwner != null)
+            {
+                serviceOwner.StorageProviders = storageProviders;
+            }
 
-        return serviceOwner;
+            return serviceOwner;
+        });
path: src/Altinn.Broker.Persistence/Repositories/ServiceOwnerRepository.cs
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Needs asynchronous reader and cancellation token support**

There are two issues with the current implementation:
1. The method uses synchronous `reader.Read()` instead of `await reader.ReadAsync(ct)` which is inconsistent with other repositories and doesn't fully leverage asynchronous operations.
2. It doesn't pass a cancellation token to the retry executor, which is inconsistent with how other repositories handle asynchronous operations.


Modify the method to use `ReadAsync` and include a cancellation token parameter:

```diff
-public async Task<ServiceOwnerEntity?> GetServiceOwner(string serviceOwnerId)
+public async Task<ServiceOwnerEntity?> GetServiceOwner(string serviceOwnerId, CancellationToken cancellationToken = default)
 {
     await using var command = dataSource.CreateCommand(
         "SELECT service_owner_id_pk, service_owner_name, " +
         "storage_provider_id_pk, created, resource_name, storage_provider_type, active " +
         "FROM broker.service_owner " +
         "LEFT JOIN broker.storage_provider sp on sp.service_owner_id_fk = service_owner_id_pk " +
         "WHERE service_owner_id_pk = @serviceOwnerId " +
         "ORDER BY created desc");
     command.Parameters.AddWithValue("@serviceOwnerId", serviceOwnerId);

-    return await commandExecutor.ExecuteWithRetry(async (ct) =>
+    return await commandExecutor.ExecuteWithRetry(async (ct) =>
     {
         ServiceOwnerEntity? serviceOwner = null;
         var storageProviders = new List<StorageProviderEntity>();

         await using var reader = await command.ExecuteReaderAsync(ct);
-        while (reader.Read())
+        while (await reader.ReadAsync(ct))
         {
             // Rest of the implementation remains the same
         }
         // Rest of the implementation remains the same
         return serviceOwner;
-    });
+    }, cancellationToken);
 }
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
public async Task<ServiceOwnerEntity?> GetServiceOwner(string serviceOwnerId, CancellationToken cancellationToken = default)
{
    await using var command = dataSource.CreateCommand(
        "SELECT service_owner_id_pk, service_owner_name, " +
        "storage_provider_id_pk, created, resource_name, storage_provider_type, active " +
        "FROM broker.service_owner " +
        "LEFT JOIN broker.storage_provider sp on sp.service_owner_id_fk = service_owner_id_pk " +
        "WHERE service_owner_id_pk = @serviceOwnerId " +
        "ORDER BY created desc");
    command.Parameters.AddWithValue("@serviceOwnerId", serviceOwnerId);

    return await commandExecutor.ExecuteWithRetry(async (ct) =>
    {
        ServiceOwnerEntity? serviceOwner = null;
        var storageProviders = new List<StorageProviderEntity>();

        await using var reader = await command.ExecuteReaderAsync(ct);
        while (await reader.ReadAsync(ct))
        {
            if (serviceOwner == null)
            {
                serviceOwner = new ServiceOwnerEntity
                {
                    Id = reader.GetString(reader.GetOrdinal("service_owner_id_pk")),
                    Name = reader.GetString(reader.GetOrdinal("service_owner_name")),
                    StorageProviders = new List<StorageProviderEntity>()
                };
            }

            if (!reader.IsDBNull(reader.GetOrdinal("storage_provider_id_pk")))
            {
                var storageProvider = new StorageProviderEntity
                {
                    ServiceOwnerId = serviceOwnerId,
                    Created = reader.GetDateTime(reader.GetOrdinal("created")),
                    Id = reader.GetInt64(reader.GetOrdinal("storage_provider_id_pk")),
                    ResourceName = reader.GetString(reader.GetOrdinal("resource_name")),
                    Type = Enum.Parse<StorageProviderType>(reader.GetString(reader.GetOrdinal("storage_provider_type"))),
                    Active = reader.GetBoolean(reader.GetOrdinal("active"))
                };

                if (storageProvider.Active && !storageProviders.Any(sp => sp.Id == storageProvider.Id))
                {
                    storageProviders.Add(storageProvider);
                }
            }
        }

        if (serviceOwner != null)
        {
            serviceOwner.StorageProviders = storageProviders;
        }

        return serviceOwner;
    }, cancellationToken);
}
`````

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents (early access)</summary>

```
In src/Altinn.Broker.Persistence/Repositories/ServiceOwnerRepository.cs between
lines 21 and 64, update the data reader loop to use the asynchronous method
ReadAsync with the cancellation token instead of the synchronous Read method.
Also, modify the method signature to accept a CancellationToken parameter and
pass this token to the ExecuteWithRetry method to ensure proper cancellation
support and consistency with other repositories.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-12T13:24:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -17,47 +18,50 @@ public class ServiceOwnerRepository(NpgsqlDataSource dataSource) : IServiceOwner
             "ORDER BY created desc");
         command.Parameters.AddWithValue("@serviceOwnerId", serviceOwnerId);
 
-        using NpgsqlDataReader reader = await command.ExecuteReaderAsync();
-        ServiceOwnerEntity? serviceOwner = null;
-        var storageProviders = new List<StorageProviderEntity>();
-
-        while (reader.Read())
+        return await commandExecutor.ExecuteWithRetry(async (ct) =>
         {
-            if (serviceOwner == null)
-            {
-                serviceOwner = new ServiceOwnerEntity
-                {
-                    Id = reader.GetString(reader.GetOrdinal("service_owner_id_pk")),
-                    Name = reader.GetString(reader.GetOrdinal("service_owner_name")),
-                    StorageProviders = new List<StorageProviderEntity>()
-                };
-            }
+            ServiceOwnerEntity? serviceOwner = null;
+            var storageProviders = new List<StorageProviderEntity>();
 
-            if (!reader.IsDBNull(reader.GetOrdinal("storage_provider_id_pk")))
+            await using var reader = await command.ExecuteReaderAsync(ct);
+            while (reader.Read())
             {
-                var storageProvider = new StorageProviderEntity
+                if (serviceOwner == null)
                 {
-                    ServiceOwnerId = serviceOwnerId,
-                    Created = reader.GetDateTime(reader.GetOrdinal("created")),
-                    Id = reader.GetInt64(reader.GetOrdinal("storage_provider_id_pk")),
-                    ResourceName = reader.GetString(reader.GetOrdinal("resource_name")),
-                    Type = Enum.Parse<StorageProviderType>(reader.GetString(reader.GetOrdinal("storage_provider_type"))),
-                    Active = reader.GetBoolean(reader.GetOrdinal("active"))
-                };
+                    serviceOwner = new ServiceOwnerEntity
+                    {
+                        Id = reader.GetString(reader.GetOrdinal("service_owner_id_pk")),
+                        Name = reader.GetString(reader.GetOrdinal("service_owner_name")),
+                        StorageProviders = new List<StorageProviderEntity>()
+                    };
+                }
 
-                if (storageProvider.Active && !storageProviders.Any(sp => sp.Id == storageProvider.Id))
+                if (!reader.IsDBNull(reader.GetOrdinal("storage_provider_id_pk")))
                 {
-                    storageProviders.Add(storageProvider);
+                    var storageProvider = new StorageProviderEntity
+                    {
+                        ServiceOwnerId = serviceOwnerId,
+                        Created = reader.GetDateTime(reader.GetOrdinal("created")),
+                        Id = reader.GetInt64(reader.GetOrdinal("storage_provider_id_pk")),
+                        ResourceName = reader.GetString(reader.GetOrdinal("resource_name")),
+                        Type = Enum.Parse<StorageProviderType>(reader.GetString(reader.GetOrdinal("storage_provider_type"))),
+                        Active = reader.GetBoolean(reader.GetOrdinal("active"))
+                    };
+
+                    if (storageProvider.Active && !storageProviders.Any(sp => sp.Id == storageProvider.Id))
path: src/Altinn.Broker.Persistence/Repositories/ServiceOwnerRepository.cs
user: login: Ceredron
body: We should add storage providers to service owners regardless of whether or not they are flagged as active.
created_at: 2025-05-13T08:38:21Z
author_association: COLLABORATOR

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-05-12T13:18:04Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

<details>
<summary>📝 Walkthrough</summary>

"""
## Walkthrough

A new helper class, `ExecuteDBCommandWithRetries`, was added to encapsulate retry logic for transient database failures using Polly. All repository classes in the persistence layer were refactored to accept this class via constructor injection and to use its retry-enabled methods for executing database commands. Dependency injection was updated accordingly.

## Changes

| Files/Areas                                                                                     | Change Summary                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
|-------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `src/Altinn.Broker.Persistence/Helpers/ExecuteDBCommandWithRetries.cs`                          | Introduced new public class `ExecuteDBCommandWithRetries` providing async retry logic for database operations using Polly. Exposes two public methods for retrying commands returning a result or not, with logging and configurable retry policy.                                                                                            |
| `src/Altinn.Broker.Persistence/DependencyInjection.cs`                                          | Registered `ExecuteDBCommandWithRetries` as a singleton in the `AddPersistence` method alongside other persistence services.                                                                                                                                                                                                                                                                                                                                       |
| `src/Altinn.Broker.Persistence/Repositories/ActorFileTransferStatusRepository.cs`<br>`.../ActorRepository.cs`<br>`.../FileTransferRepository.cs`<br>`.../FileTransferStatusRepository.cs`<br>`.../IdempotencyEventRepository.cs`<br>`.../PartyRepository.cs`<br>`.../ResourceRepository.cs`<br>`.../ServiceOwnerRepository.cs` | Updated constructors to accept `ExecuteDBCommandWithRetries` as a dependency. Refactored all database command executions to use the retry executor's methods, replacing direct calls to `ExecuteReaderAsync`, `ExecuteNonQueryAsync`, and `ExecuteScalarAsync`. Internal logic and SQL unchanged; now resilient to transient database errors. |

## Suggested labels

`kind/chore`

## Suggested reviewers

- Ceredron


</details>

<!-- walkthrough_end -->

<!-- announcements_start -->

> [!TIP]
> <details>
> <summary>⚡️ Faster reviews with caching</summary>
> 
> - CodeRabbit now supports caching for code and dependencies, helping speed up reviews. This means quicker feedback, reduced wait times, and a smoother review experience overall. Cached data is encrypted and stored securely. This feature will be automatically enabled for all accounts on May 16th. To opt out, configure `Review - Disable Cache` at either the organization or repository level. If you prefer to disable all data retention across your organization, simply turn off the `Data Retention` setting under your Organization Settings.
> 
> Enjoy the performance boost—your workflow just got faster.
> 
> </details>

<!-- announcements_end -->

---

<details>
<summary>📜 Recent review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 1d2fe5d77c0835f1095ea95ebf4a7a6c626d5fca and cd915596087c1be9b5dd88f5ee092d389215e550.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `src/Altinn.Broker.Persistence/Helpers/ExecuteDBCommandWithRetries.cs` (1 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary>

* src/Altinn.Broker.Persistence/Helpers/ExecuteDBCommandWithRetries.cs

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (1)</summary>

* GitHub Check: Analyze (csharp)

</details>

</details>
<!-- internal state start -->


<!-- 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 -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details open="true">
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=Altinn/altinn-broker&utm_content=723):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Explain this complex logic.`
  - `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 explain this code block.`
  -	`@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 explain its main purpose.`
  - `@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.`

### Support

Need help? Create a ticket on our [support page](https://www.coderabbit.ai/contact-us/support) for assistance with any issues or questions.

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](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@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.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR
user: login: Ceredron
created_at: 2025-05-13T08:40:32Z
body: Are we still using transactions, or is CommandExecutor circumventing it? I think all these operations are usually already baked into a transaction on the Application layer that itself is retried. 

Will it be a trouble with nested retries?
author_association: COLLABORATOR
user: login: axely123
created_at: 2025-05-13T11:41:14Z
body: > Are we still using transactions, or is CommandExecutor circumventing it? I think all these operations are usually already baked into a transaction on the Application layer that itself is retried.

I don't think all Npgsql exceptions are retried by the transactions currently as they are not in the retry policy, only postgresexceptions are already included. Some of the npgsql exceptions but not all are transient, meaning they might be solved by retries. NpgsqlExceptions have a IsTransient method for this, which checks if a given npgsqlexception is considered transient by npgsql. Transient npgsql exceptions should include timeout exceptions and ssl exceptions for example. Both of these exceptions has been encountered both in and outside of transactions in production. 

Some examples:
* [Inner Npgsql exception stemming from timeout occured in hangfire job](https://portal.azure.com/#view/AppInsightsExtension/DetailsV2Blade/ComponentId~/%7B%22SubscriptionId%22%3A%22854b0f13-008f-42f1-882d-8813b1af27aa%22%2C%22ResourceGroup%22%3A%22altinn-broker-prod-rg%22%2C%22Name%22%3A%22altinn-broker-prod-ai%22%2C%22LinkedApplicationType%22%3A0%2C%22ResourceId%22%3A%22%252Fsubscriptions%252F854b0f13-008f-42f1-882d-8813b1af27aa%252FresourceGroups%252Faltinn-broker-prod-rg%252Fproviders%252FMicrosoft.Insights%252Fcomponents%252Faltinn-broker-prod-ai%22%2C%22ResourceType%22%3A%22microsoft.insights%252Fcomponents%22%2C%22IsAzureFirst%22%3Afalse%7D/DataModel~/%7B%22eventId%22%3A%2240788294-2fbc-11f0-9a40-0022486fe08f%22%2C%22timestamp%22%3A%222025-05-13T05%3A35%3A58.264Z%22%2C%22cacheId%22%3A%22041d4617-f50b-4b0a-8aa0-2ce6ce69660b%22%2C%22eventTable%22%3A%22exceptions%22%7D)(Outside transaction) (Occured during partyRepository.GetParty)
* [Npgsql exception stemming from timeout occured inside transaction](https://portal.azure.com/#view/AppInsightsExtension/DetailsV2Blade/ComponentId~/%7B%22SubscriptionId%22%3A%22854b0f13-008f-42f1-882d-8813b1af27aa%22%2C%22ResourceGroup%22%3A%22altinn-broker-prod-rg%22%2C%22Name%22%3A%22altinn-broker-prod-ai%22%2C%22LinkedApplicationType%22%3A0%2C%22ResourceId%22%3A%22%252Fsubscriptions%252F854b0f13-008f-42f1-882d-8813b1af27aa%252FresourceGroups%252Faltinn-broker-prod-rg%252Fproviders%252FMicrosoft.Insights%252Fcomponents%252Faltinn-broker-prod-ai%22%2C%22ResourceType%22%3A%22microsoft.insights%252Fcomponents%22%2C%22IsAzureFirst%22%3Afalse%7D/DataModel~/%7B%22eventId%22%3A%22eb777c9d-2e22-11f0-9a40-000d3a87991a%22%2C%22timestamp%22%3A%222025-05-11T04%3A45%3A55.637Z%22%2C%22cacheId%22%3A%2240359e0c-cee1-433e-b844-a34099d3e512%22%2C%22eventTable%22%3A%22exceptions%22%7D)(I don't think this exception is retried by the transaction) (seems to have occured during fileTransferRepository.GetFileTransfer operation)
* [Npgsql exception stemming from timeout outside transaction and not in a hangfireJob](https://portal.azure.com/#view/AppInsightsExtension/DetailsV2Blade/ComponentId~/%7B%22SubscriptionId%22%3A%22854b0f13-008f-42f1-882d-8813b1af27aa%22%2C%22ResourceGroup%22%3A%22altinn-broker-prod-rg%22%2C%22Name%22%3A%22altinn-broker-prod-ai%22%2C%22LinkedApplicationType%22%3A0%2C%22ResourceId%22%3A%22%252Fsubscriptions%252F854b0f13-008f-42f1-882d-8813b1af27aa%252FresourceGroups%252Faltinn-broker-prod-rg%252Fproviders%252FMicrosoft.Insights%252Fcomponents%252Faltinn-broker-prod-ai%22%2C%22ResourceType%22%3A%22microsoft.insights%252Fcomponents%22%2C%22IsAzureFirst%22%3Afalse%7D/DataModel~/%7B%22eventId%22%3A%22e6dc4ae9-2f43-11f0-9a40-6045bdc6b2a2%22%2C%22timestamp%22%3A%222025-05-12T15%3A13%3A58.238Z%22%2C%22cacheId%22%3A%2296a2bf05-9d82-42af-a9fb-9471ccb69e22%22%2C%22eventTable%22%3A%22exceptions%22%7D) (This occured during a actorRepository operation)
* [Npgsql SSL handshake related exception occured inside transaction](https://portal.azure.com/#view/AppInsightsExtension/DetailsV2Blade/ComponentId~/%7B%22SubscriptionId%22%3A%22854b0f13-008f-42f1-882d-8813b1af27aa%22%2C%22ResourceGroup%22%3A%22altinn-broker-prod-rg%22%2C%22Name%22%3A%22altinn-broker-prod-ai%22%2C%22LinkedApplicationType%22%3A0%2C%22ResourceId%22%3A%22%252Fsubscriptions%252F854b0f13-008f-42f1-882d-8813b1af27aa%252FresourceGroups%252Faltinn-broker-prod-rg%252Fproviders%252FMicrosoft.Insights%252Fcomponents%252Faltinn-broker-prod-ai%22%2C%22ResourceType%22%3A%22microsoft.insights%252Fcomponents%22%2C%22IsAzureFirst%22%3Afalse%7D/DataModel~/%7B%22eventId%22%3A%22d7a63f5e-2e2a-11f0-9a40-000d3a8791b1%22%2C%22timestamp%22%3A%222025-05-11T05%3A41%3A35.178Z%22%2C%22cacheId%22%3A%22ca5ad631-fce0-40f6-8a5f-77d87fd76da6%22%2C%22eventTable%22%3A%22exceptions%22%7D) (seems to have occured on fileTransferRepository.GetFileTransfer operation)
* [Npgsql SSL handshake related exception occured outside transaction in malwareScanningResultHandler](https://portal.azure.com/#view/AppInsightsExtension/DetailsV2Blade/ComponentId~/%7B%22SubscriptionId%22%3A%22854b0f13-008f-42f1-882d-8813b1af27aa%22%2C%22ResourceGroup%22%3A%22altinn-broker-prod-rg%22%2C%22Name%22%3A%22altinn-broker-prod-ai%22%2C%22LinkedApplicationType%22%3A0%2C%22ResourceId%22%3A%22%252Fsubscriptions%252F854b0f13-008f-42f1-882d-8813b1af27aa%252FresourceGroups%252Faltinn-broker-prod-rg%252Fproviders%252FMicrosoft.Insights%252Fcomponents%252Faltinn-broker-prod-ai%22%2C%22ResourceType%22%3A%22microsoft.insights%252Fcomponents%22%2C%22IsAzureFirst%22%3Afalse%7D/DataModel~/%7B%22eventId%22%3A%22fb168b2b-2b07-11f0-9a40-0022486ff031%22%2C%22timestamp%22%3A%222025-05-07T05%3A55%3A24.783Z%22%2C%22cacheId%22%3A%22b923cda8-16d7-4c84-adef-c383f40141c4%22%2C%22eventTable%22%3A%22exceptions%22%7D) (Seems to have occured on filetransferRepository.GetFileTransfer operation)

Because alot of the issues also happen outside the transaction with retries, I figured it would be better to have a retry mechanism at the repo level for the transient errors and with very few retries. This would also handle the transient errors for the Database connection that happens outside the transactions.
> Will it be a trouble with nested retries?

The commandExecutor should not circumvent the transactions as the exceptions it does not handle should bubble up into the transaction and get handled by it. 

Also all database operations within the transaction scopes, including those executed through the commandExecutor, will be part of the same transaction. So the transactions should work as normal.
author_association: COLLABORATOR
user: login: axely123
created_at: 2025-05-13T12:02:16Z
body: I tried to figure out why the timout exceptions occured also. I believe its a connection issue and not a performance issue since the timeout has occured on so many different DB operations. But I tried to analyze the performance on GetFileTransfer as an example and it shouldn't ever take more than a second (timeouts occur after 15 seconds). The relevant fields filtered in the query are already indexed, except for fs.file_transfer_status_date which is filtered after filetransfer_id has been filtered for so it shouldn't need to be indexed. And when testing the query directly in database it takes just milliseconds.

author_association: COLLABORATOR
user: login: Ceredron
created_at: 2025-05-14T06:22:31Z
body: > I tried to figure out why the timout exceptions occured also. I believe its a connection issue and not a performance issue since the timeout has occured on so many different DB operations. But I tried to analyze the performance on GetFileTransfer as an example and it shouldn't ever take more than a second (timeouts occur after 15 seconds). The relevant fields filtered in the query are already indexed, except for fs.file_transfer_status_date which is filtered after filetransfer_id has been filtered for so it shouldn't need to be indexed. And when testing the query directly in database it takes just milliseconds.

That makes sense. I agree about GetFileTransfer. 
author_association: COLLABORATOR