Repository: openops-cloud/openops
Pull request #429: Retry failing 409 requests to tables service during app seeding phase
Author: login: cezudas
Created: 2025-04-10T07:29:43Z | Merged: 2025-04-11T09:01:20Z

Fixes OPS-1311.


Diff:
diff --git a/packages/openops/src/lib/axios-wrapper.ts b/packages/openops/src/lib/axios-wrapper.ts
index ebbcdd14da..51b22a4af0 100644
--- a/packages/openops/src/lib/axios-wrapper.ts
+++ b/packages/openops/src/lib/axios-wrapper.ts
@@ -1,22 +1,34 @@
 import { logger } from '@openops/server-shared';
 import axios, { AxiosError, AxiosHeaders, Method } from 'axios';
+import axiosRetry, { IAxiosRetryConfig } from 'axios-retry';
+
+function getRetryAxiosInstance(retryConfigs: IAxiosRetryConfig) {
+  const retryAxiosInstance = axios.create();
+
+  axiosRetry(retryAxiosInstance, retryConfigs);
+  return retryAxiosInstance;
+}
 
 export async function makeHttpRequest<T>(
   method: Method,
   url: string,
   headers?: AxiosHeaders,
   body?: any,
+  retryConfigs?: IAxiosRetryConfig,
 ): Promise<T> {
   try {
     const config = {
-      data: body,
-      headers,
       method,
       url,
+      headers,
+      data: body,
     };
 
-    const response = await axios(config);
+    const axiosInstance = retryConfigs
+      ? getRetryAxiosInstance(retryConfigs)
+      : axios;
 
+    const response = await axiosInstance.request(config);
     return response.data;
   } catch (error) {
     const axiosError = error as AxiosError;
diff --git a/packages/openops/src/lib/openops-tables/applications-service.ts b/packages/openops/src/lib/openops-tables/applications-service.ts
index bc623b4453..82b079f408 100644
--- a/packages/openops/src/lib/openops-tables/applications-service.ts
+++ b/packages/openops/src/lib/openops-tables/applications-service.ts
@@ -1,4 +1,8 @@
-import { createAxiosHeaders, makeOpenOpsTablesGet } from './requests-helpers';
+import {
+  axiosTablesSeedRetryConfig,
+  createAxiosHeaders,
+  makeOpenOpsTablesGet,
+} from './requests-helpers';
 import { Application } from './types';
 
 export const OPENOPS_DEFAULT_DATABASE_NAME = 'OpenOps Dataset';
@@ -13,6 +17,7 @@ export async function getDefaultDatabaseId(
     await makeOpenOpsTablesGet<Application>(
       `api/applications/`,
       authenticationHeader,
+      axiosTablesSeedRetryConfig,
     );
 
   const defaultDatabase: Application | undefined = getTablesResult
diff --git a/packages/openops/src/lib/openops-tables/auth-user.ts b/packages/openops/src/lib/openops-tables/auth-user.ts
index 9aeff25db3..63e64b1cd9 100644
--- a/packages/openops/src/lib/openops-tables/auth-user.ts
+++ b/packages/openops/src/lib/openops-tables/auth-user.ts
@@ -1,7 +1,7 @@
 import { AppSystemProp, cacheWrapper, system } from '@openops/server-shared';
 import { AxiosHeaders } from 'axios';
+import { IAxiosRetryConfig } from 'axios-retry';
 import { makeOpenOpsTablesPost } from './requests-helpers';
-
 const tokenLifetimeMinutes = system.getNumber(
   AppSystemProp.TABLES_TOKEN_LIFETIME_MINUTES,
 );
@@ -18,6 +18,7 @@ export interface AuthTokens {
 export async function authenticateUserInOpenOpsTables(
   email: string,
   password: string,
+  axiosRetryConfig?: IAxiosRetryConfig,
 ): Promise<AuthTokens> {
   const requestBody = {
     email,
@@ -32,10 +33,13 @@ export async function authenticateUserInOpenOpsTables(
     'api/user/token-auth/',
     requestBody,
     headers,
+    axiosRetryConfig,
   );
 }
 
-export async function authenticateDefaultUserInOpenOpsTables(): Promise<AuthTokens> {
+export async function authenticateDefaultUserInOpenOpsTables(
+  axiosRetryConfig?: IAxiosRetryConfig,
+): Promise<AuthTokens> {
   const cacheKey = 'openops-tables-token';
 
   let tokens = await cacheWrapper.getSerializedObject<AuthTokens>(cacheKey);
@@ -44,7 +48,11 @@ export async function authenticateDefaultUserInOpenOpsTables(): Promise<AuthToke
     const email = system.getOrThrow(AppSystemProp.OPENOPS_ADMIN_EMAIL);
     const password = system.getOrThrow(AppSystemProp.OPENOPS_ADMIN_PASSWORD);
 
-    tokens = await authenticateUserInOpenOpsTables(email, password);
+    tokens = await authenticateUserInOpenOpsTables(
+      email,
+      password,
+      axiosRetryConfig,
+    );
     await cacheWrapper.setSerializedObject(
       cacheKey,
       tokens,
diff --git a/packages/openops/src/lib/openops-tables/fields.ts b/packages/openops/src/lib/openops-tables/fields.ts
index a1ea72b7d0..6a990013a7 100644
--- a/packages/openops/src/lib/openops-tables/fields.ts
+++ b/packages/openops/src/lib/openops-tables/fields.ts
@@ -1,3 +1,4 @@
+import { IAxiosRetryConfig } from 'axios-retry';
 import {
   createAxiosHeaders,
   makeOpenOpsTablesGet,
@@ -7,11 +8,13 @@ export async function getFields(
   tableId: number,
   token: string,
   userFieldNames = true,
+  axiosRetryConfig?: IAxiosRetryConfig,
 ): Promise<OpenOpsField[]> {
   const authenticationHeader = createAxiosHeaders(token);
   const fields = await makeOpenOpsTablesGet<any[]>(
     `api/database/fields/table/${tableId}/?user_field_names=${userFieldNames}`,
     authenticationHeader,
+    axiosRetryConfig,
   );
 
   return fields.flatMap((item) => item);
diff --git a/packages/openops/src/lib/openops-tables/openops-tables-common-properties.ts b/packages/openops/src/lib/openops-tables/openops-tables-common-properties.ts
index 226ae863fe..2246f65157 100644
--- a/packages/openops/src/lib/openops-tables/openops-tables-common-properties.ts
+++ b/packages/openops/src/lib/openops-tables/openops-tables-common-properties.ts
@@ -1,4 +1,5 @@
 import { Property, Validators } from '@openops/blocks-framework';
+import { IAxiosRetryConfig } from 'axios-retry';
 import { authenticateDefaultUserInOpenOpsTables } from './auth-user';
 import {
   DateOpenOpsField,
@@ -34,12 +35,13 @@ export function openopsTablesDropdownProperty(): any {
 
 export async function getTableFields(
   tableName: string,
+  axiosRetryConfig?: IAxiosRetryConfig,
 ): Promise<OpenOpsField[]> {
   const { token } = await authenticateDefaultUserInOpenOpsTables();
 
   const tableId = await getTableIdByTableName(tableName as unknown as string);
 
-  return await getFields(tableId, token);
+  return await getFields(tableId, token, false, axiosRetryConfig);
 }
 
 // https://api.baserow.io/api/redoc/#tag/Database-table-fields/operation/get_database_table_field
diff --git a/packages/openops/src/lib/openops-tables/requests-helpers.ts b/packages/openops/src/lib/openops-tables/requests-helpers.ts
index c201aac593..6925b00ed0 100644
--- a/packages/openops/src/lib/openops-tables/requests-helpers.ts
+++ b/packages/openops/src/lib/openops-tables/requests-helpers.ts
@@ -1,5 +1,8 @@
-import { AppSystemProp, system } from '@openops/server-shared';
-import { AxiosHeaders, AxiosResponse, Method } from 'axios';
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import { AppSystemProp, logger, system } from '@openops/server-shared';
+import { AxiosError, AxiosHeaders, AxiosResponse, Method } from 'axios';
+import { IAxiosRetryConfig } from 'axios-retry';
+import { StatusCodes } from 'http-status-codes';
 import { makeHttpRequest } from '../axios-wrapper';
 
 export function createAxiosHeaders(token: string): AxiosHeaders {
@@ -9,34 +12,74 @@ export function createAxiosHeaders(token: string): AxiosHeaders {
   });
 }
 
-// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export const axiosTablesSeedRetryConfig: IAxiosRetryConfig = {
+  retries: 3,
+  retryDelay: (retryCount: number) => {
+    logger.debug(
+      `The request failed due to a conflict. Request count: ${retryCount}`,
+    );
+    return retryCount * 1000;
+  },
+  retryCondition: (error: AxiosError) => {
+    return (
+      (error?.response?.status &&
+        error?.response?.status === StatusCodes.CONFLICT) ||
+      false
+    );
+  },
+};
+
 export async function makeOpenOpsTablesPost<T>(
   route: string,
   body: any,
   headers?: AxiosHeaders,
+  retryConfigs?: IAxiosRetryConfig,
 ): Promise<T> {
-  return makeOpenOpsTablesRequest<T>('POST', route, body, headers);
+  return makeOpenOpsTablesRequest<T>(
+    'POST',
+    route,
+    body,
+    headers,
+    undefined,
+    retryConfigs,
+  );
 }
 
 export async function makeOpenOpsTablesDelete<T>(
   route: string,
   headers?: AxiosHeaders,
+  retryConfigs?: IAxiosRetryConfig,
 ): Promise<T> {
-  return makeOpenOpsTablesRequest<T>('DELETE', route, undefined, headers);
+  return makeOpenOpsTablesRequest<T>(
+    'DELETE',
+    route,
+    undefined,
+    headers,
+    undefined,
+    retryConfigs,
+  );
 }
 
-// eslint-disable-next-line @typescript-eslint/no-explicit-any
 export async function makeOpenOpsTablesPatch<T>(
   route: string,
   body: any,
   headers?: AxiosHeaders,
+  retryConfigs?: IAxiosRetryConfig,
 ): Promise<T> {
-  return makeOpenOpsTablesRequest<T>('PATCH', route, body, headers);
+  return makeOpenOpsTablesRequest<T>(
+    'PATCH',
+    route,
+    body,
+    headers,
+    undefined,
+    retryConfigs,
+  );
 }
 
 export async function makeOpenOpsTablesGet<T>(
   route: string,
   headers?: AxiosHeaders,
+  retryConfigs?: IAxiosRetryConfig,
 ): Promise<T[]> {
   const responses: T[] = [];
   let url;
@@ -48,6 +91,7 @@ export async function makeOpenOpsTablesGet<T>(
       undefined,
       headers,
       url,
+      retryConfigs,
     );
 
     if (!response) {
@@ -67,13 +111,13 @@ export async function makeOpenOpsTablesGet<T>(
   return responses;
 }
 
-// eslint-disable-next-line @typescript-eslint/no-explicit-any
 async function makeOpenOpsTablesRequest<T>(
   method: Method,
   route: string,
   body?: any,
   headers?: AxiosHeaders,
   url?: string,
+  retryConfigs?: IAxiosRetryConfig,
 ): Promise<T> {
   const baseUrl = system.get(AppSystemProp.OPENOPS_TABLES_API_URL);
 
@@ -82,5 +126,6 @@ async function makeOpenOpsTablesRequest<T>(
     url ?? `${baseUrl}/openops-tables/${route}`,
     headers,
     body,
+    retryConfigs,
   );
 }
diff --git a/packages/openops/test/axios-wrapper.test.ts b/packages/openops/test/axios-wrapper.test.ts
index 4796010736..9d49e24019 100644
--- a/packages/openops/test/axios-wrapper.test.ts
+++ b/packages/openops/test/axios-wrapper.test.ts
@@ -1,64 +1,80 @@
-const loggerMock = {
-  error: jest.fn(),
-};
-
-jest.mock('@openops/server-shared', () => ({
-  logger: loggerMock,
-}));
-
-const axiosMock = jest.fn();
-jest.mock('axios', () => ({
-  ...jest.requireActual('axios'),
-  __esModule: true,
-  default: axiosMock,
-}));
-
-import { AxiosHeaders } from 'axios';
+import axios from 'axios';
 import { makeHttpRequest } from '../src/lib/axios-wrapper';
 
-describe('axios request', () => {
-  const header = new AxiosHeaders({ some: 'header' });
+jest.mock('axios');
 
-  describe('makeHttpRequest', () => {
-    beforeEach(() => {
-      jest.clearAllMocks();
+const mockedAxios = axios as jest.Mocked<typeof axios>;
+
+describe('makeHttpRequest', () => {
+  const mockStandardRequest = jest.fn();
+  const mockRetryRequest = jest.fn();
+
+  beforeEach(() => {
+    jest.clearAllMocks();
+
+    (axios.create as jest.Mock).mockReturnValue({
+      request: mockRetryRequest,
+      interceptors: {
+        request: { use: jest.fn(), eject: jest.fn() },
+        response: { use: jest.fn(), eject: jest.fn() },
+      },
     });
 
-    test('should return data after successful post', async () => {
-      axiosMock.mockResolvedValue({
-        data: { test: 'some data' },
-      });
-      const result = await makeHttpRequest('POST', 'testUrl', header, {
-        body: 'info',
-      });
-
-      expect(result).toEqual({ test: 'some data' });
-      expect(axiosMock).toHaveBeenCalledWith({
-        method: 'POST',
-        url: 'testUrl',
-        data: { body: 'info' },
-        headers: header,
-      });
-      expect(loggerMock.error).not.toHaveBeenCalled();
+    mockedAxios.request.mockImplementation(mockStandardRequest);
+  });
+
+  it('makes a standard request without retry config', async () => {
+    const responseData = { message: 'standard success' };
+    mockStandardRequest.mockResolvedValueOnce({ data: responseData });
+
+    const result = await makeHttpRequest('GET', 'https://example.com');
+
+    expect(mockStandardRequest).toHaveBeenCalledWith({
+      method: 'GET',
+      url: 'https://example.com',
+      headers: undefined,
+      data: undefined,
     });
 
-    test('should log an error and throw an exception on failed get', async () => {
-      axiosMock.mockRejectedValue(new Error('mock error'));
-
-      await expect(
-        makeHttpRequest('PATCH', 'testUrl', header, { body: 'info' }),
-      ).rejects.toThrow('mock error');
-
-      expect(axiosMock).toHaveBeenCalledWith({
-        method: 'PATCH',
-        url: 'testUrl',
-        data: { body: 'info' },
-        headers: header,
-      });
-      expect(loggerMock.error).toHaveBeenCalledWith(
-        `Error making HTTP request. Url: "testUrl"`,
-        expect.any(Error),
-      );
+    expect(result).toEqual(responseData);
+  });
+
+  it('makes a retry request with retry config', async () => {
+    const responseData = { message: 'retry success' };
+    mockRetryRequest.mockResolvedValueOnce({ data: responseData });
+
+    const result = await makeHttpRequest(
+      'GET',
+      'https://example.com/retry',
+      undefined,
+      undefined,
+      { retries: 3 },
+    );
+
+    expect(mockRetryRequest).toHaveBeenCalledWith({
+      method: 'GET',
+      url: 'https://example.com/retry',
+      headers: undefined,
+      data: undefined,
     });
+
+    expect(result).toEqual(responseData);
+  });
+
+  it('throws and logs error on failed request', async () => {
+    const mockError = {
+      isAxiosError: true,
+      response: {
+        data: { error: 'Something went wrong' },
+        status: 500,
+        statusText: 'Internal Server Error',
+      },
+    };
+
+    mockStandardRequest.mockRejectedValueOnce(mockError as any);
+
+    await expect(
+      makeHttpRequest('GET', 'https://example.com/fail'),
+    ).rejects.toThrow(JSON.stringify(mockError.response.data));
   });
 });
diff --git a/packages/openops/test/openops-tables/application-service.test.ts b/packages/openops/test/openops-tables/application-service.test.ts
index c3d31bb9ae..56823036b8 100644
--- a/packages/openops/test/openops-tables/application-service.test.ts
+++ b/packages/openops/test/openops-tables/application-service.test.ts
@@ -46,6 +46,7 @@ describe('getDefaultDatabaseId', () => {
     expect(makeOpenOpsTablesGetMock).toHaveBeenCalledWith(
       'api/applications/',
       'some header',
+      undefined,
     );
     expect(createAxiosHeadersMock).toBeCalledTimes(1);
     expect(createAxiosHeadersMock).toHaveBeenCalledWith('token');
@@ -73,6 +74,7 @@ describe('getDefaultDatabaseId', () => {
     expect(makeOpenOpsTablesGetMock).toHaveBeenCalledWith(
       'api/applications/',
       'some header',
+      undefined,
     );
     expect(createAxiosHeadersMock).toBeCalledTimes(1);
     expect(createAxiosHeadersMock).toHaveBeenCalledWith('token');
diff --git a/packages/openops/test/openops-tables/auth-user.test.ts b/packages/openops/test/openops-tables/auth-user.test.ts
index 5106dca249..99b0098460 100644
--- a/packages/openops/test/openops-tables/auth-user.test.ts
+++ b/packages/openops/test/openops-tables/auth-user.test.ts
@@ -41,6 +41,7 @@ describe('authUser', () => {
       'api/user/token-auth/',
       { email, password },
       new AxiosHeaders({ 'Content-Type': 'application/json' }),
+      undefined,
     );
   });
 
@@ -59,6 +60,7 @@ describe('authUser', () => {
       'api/user/token-auth/',
       { email, password },
       new AxiosHeaders({ 'Content-Type': 'application/json' }),
+      undefined,
     );
   });
 });
diff --git a/packages/openops/test/openops-tables/fields.test.ts b/packages/openops/test/openops-tables/fields.test.ts
index 799ea6485d..115d4362e8 100644
--- a/packages/openops/test/openops-tables/fields.test.ts
+++ b/packages/openops/test/openops-tables/fields.test.ts
@@ -45,6 +45,7 @@ describe('getFields', () => {
     expect(makeOpenOpsTablesGetMock).toHaveBeenCalledWith(
       'api/database/fields/table/1/?user_field_names=true',
       'some header',
+      undefined,
     );
     expect(createAxiosHeadersMock).toBeCalledTimes(1);
     expect(createAxiosHeadersMock).toHaveBeenCalledWith('token');
@@ -99,6 +100,7 @@ describe('getFields', () => {
     expect(makeOpenOpsTablesGetMock).toHaveBeenCalledWith(
       'api/database/fields/table/1/?user_field_names=true',
       'some header',
+      undefined,
     );
     expect(createAxiosHeadersMock).toBeCalledTimes(1);
     expect(createAxiosHeadersMock).toHaveBeenCalledWith('token');
diff --git a/packages/openops/test/openops-tables/openops-tables-common-properties.test.ts b/packages/openops/test/openops-tables/openops-tables-common-properties.test.ts
index 952ec5bd0d..5eaee83a3e 100644
--- a/packages/openops/test/openops-tables/openops-tables-common-properties.test.ts
+++ b/packages/openops/test/openops-tables/openops-tables-common-properties.test.ts
@@ -116,7 +116,7 @@ describe('getTableFields', () => {
     ]);
     expect(authenticateDefaultUserInOpenOpsTablesMock).toHaveBeenCalledTimes(1);
     expect(getFieldsMock).toHaveBeenCalledTimes(1);
-    expect(getFieldsMock).toHaveBeenCalledWith(1, 'token'); // Adjust this line as needed.
+    expect(getFieldsMock).toHaveBeenCalledWith(1, 'token', false, undefined); // Adjust this line as needed.
   });
 
   test('should handle authentication failure', async () => {
@@ -144,7 +144,7 @@ describe('getTableFields', () => {
     expect(result).toMatchObject([]);
     expect(authenticateDefaultUserInOpenOpsTablesMock).toHaveBeenCalledTimes(1);
     expect(getFieldsMock).toHaveBeenCalledTimes(1);
-    expect(getFieldsMock).toHaveBeenCalledWith(1, 'token');
+    expect(getFieldsMock).toHaveBeenCalledWith(1, 'token', false, undefined);
   });
 
   test('should return our supported properties for the field', async () => {
diff --git a/packages/openops/test/openops-tables/request-helpers.test.ts b/packages/openops/test/openops-tables/request-helpers.test.ts
index 50151e4013..c12761b23e 100644
--- a/packages/openops/test/openops-tables/request-helpers.test.ts
+++ b/packages/openops/test/openops-tables/request-helpers.test.ts
@@ -41,6 +41,7 @@ describe('axios request', () => {
       'http://mockapi.com/openops-tables/test/route/api',
       header,
       undefined,
+      undefined,
     );
   });
 
@@ -59,6 +60,7 @@ describe('axios request', () => {
       'http://mockapi.com/openops-tables/test/route/api',
       header,
       { body: 'info' },
+      undefined,
     );
   });
 
@@ -77,6 +79,7 @@ describe('axios request', () => {
       'http://mockapi.com/openops-tables/test/route/api',
       header,
       { body: 'info' },
+      undefined,
     );
   });
 
@@ -109,6 +112,7 @@ describe('axios request', () => {
       'http://mockapi.com/openops-tables/test/route/api',
       header,
       undefined,
+      undefined,
     );
 
     expect(httpRequestMock).toHaveBeenNthCalledWith(
@@ -117,6 +121,7 @@ describe('axios request', () => {
       'next url',
       header,
       undefined,
+      undefined,
     );
 
     expect(httpRequestMock).toHaveBeenNthCalledWith(
@@ -125,6 +130,7 @@ describe('axios request', () => {
       'next url two',
       header,
       undefined,
+      undefined,
     );
   });
 });
diff --git a/packages/server/api/src/app/database/migrations/1741945618000-ReplaceSelectOptionsIdsWithNames.ts b/packages/server/api/src/app/database/migrations/1741945618000-ReplaceSelectOptionsIdsWithNames.ts
index 885c6b797b..733143f954 100644
--- a/packages/server/api/src/app/database/migrations/1741945618000-ReplaceSelectOptionsIdsWithNames.ts
+++ b/packages/server/api/src/app/database/migrations/1741945618000-ReplaceSelectOptionsIdsWithNames.ts
@@ -1,5 +1,9 @@
 /* eslint-disable @typescript-eslint/no-explicit-any */
-import { getTableFields, SelectOpenOpsField } from '@openops/common';
+import {
+  axiosTablesSeedRetryConfig,
+  getTableFields,
+  SelectOpenOpsField,
+} from '@openops/common';
 import { logger } from '@openops/server-shared';
 import { MigrationInterface, QueryRunner } from 'typeorm';
 
@@ -57,7 +61,10 @@ const getFieldsFromCache = async (tableName: string) => {
 
   let fields: SelectOpenOpsField[] = [];
   try {
-    fields = (await getTableFields(tableName)) as SelectOpenOpsField[];
+    fields = (await getTableFields(
+      tableName,
+      axiosTablesSeedRetryConfig,
+    )) as SelectOpenOpsField[];
   } catch (e) {
     logger.error(`Failed to get fields for table ${tableName}`, e);
   }
diff --git a/packages/server/api/src/app/database/seeds/openops-delete-old-opportunities-table.ts b/packages/server/api/src/app/database/seeds/openops-delete-old-opportunities-table.ts
index c2de8fd3d3..598df4cf23 100644
--- a/packages/server/api/src/app/database/seeds/openops-delete-old-opportunities-table.ts
+++ b/packages/server/api/src/app/database/seeds/openops-delete-old-opportunities-table.ts
@@ -2,6 +2,7 @@
 
 import {
   authenticateDefaultUserInOpenOpsTables,
+  axiosTablesSeedRetryConfig,
   createAxiosHeaders,
   getTableByName,
   makeOpenOpsTablesDelete,
@@ -52,7 +53,9 @@ export const deleteOldOpportunitiesTable = async (): Promise<void> => {
   }
 
   try {
-    const { token } = await authenticateDefaultUserInOpenOpsTables();
+    const { token } = await authenticateDefaultUserInOpenOpsTables(
+      axiosTablesSeedRetryConfig,
+    );
 
     const table = await getTableByName(SEED_OPENOPS_TABLE_NAME);
     if (!table) {
@@ -63,6 +66,7 @@ export const deleteOldOpportunitiesTable = async (): Promise<void> => {
       await makeOpenOpsTablesDelete<unknown>(
         `api/database/tables/${table.id}/`,
         createAxiosHeaders(token),
+        axiosTablesSeedRetryConfig,
       );
     }
 
diff --git a/packages/server/api/src/app/database/seeds/openops-tables-rename-database.ts b/packages/server/api/src/app/database/seeds/openops-tables-rename-database.ts
index 048479cafd..8eb375830b 100644
--- a/packages/server/api/src/app/database/seeds/openops-tables-rename-database.ts
+++ b/packages/server/api/src/app/database/seeds/openops-tables-rename-database.ts
@@ -7,10 +7,10 @@ import { databaseConnection } from '../database-connection';
 
 import {
   authenticateDefaultUserInOpenOpsTables,
+  axiosTablesSeedRetryConfig,
   getDefaultDatabaseId,
   OPENOPS_DEFAULT_DATABASE_NAME,
 } from '@openops/common';
-import { error } from 'console';
 
 const OPENOPS_TABLES_DATABASE_RENAMED_FLAG = 'TABLES_DB_RENAMED';
 const isOpenopsTablesDatabaseAlreadyRenamed = async (): Promise<boolean> => {
@@ -40,7 +40,9 @@ export const updateOpenopsTablesDatabase = async (): Promise<void> => {
     return;
   }
 
-  const { token } = await authenticateDefaultUserInOpenOpsTables();
+  const { token } = await authenticateDefaultUserInOpenOpsTables(
+    axiosTablesSeedRetryConfig,
+  );
 
   try {
     const tablesDatabaseId = await getDefaultDatabaseId(
diff --git a/packages/server/api/src/app/database/seeds/seed-admin.ts b/packages/server/api/src/app/database/seeds/seed-admin.ts
index 3b7874d6e1..694215675e 100644
--- a/packages/server/api/src/app/database/seeds/seed-admin.ts
+++ b/packages/server/api/src/app/database/seeds/seed-admin.ts
@@ -1,4 +1,7 @@
-import { authenticateDefaultUserInOpenOpsTables } from '@openops/common';
+import {
+  authenticateDefaultUserInOpenOpsTables,
+  axiosTablesSeedRetryConfig,
+} from '@openops/common';
 import { AppSystemProp, logger, system } from '@openops/server-shared';
 import { OrganizationRole, User } from '@openops/shared';
 import { authenticationService } from '../../authentication/authentication-service';
@@ -45,7 +48,9 @@ async function createAdmin(email: string, password: string) {
     newsLetter: false,
   });
 
-  const { token } = await authenticateDefaultUserInOpenOpsTables();
+  const { token } = await authenticateDefaultUserInOpenOpsTables(
+    axiosTablesSeedRetryConfig,
+  );
 
   const { workspaceId, databaseId } =
     await openopsTables.createDefaultWorkspaceAndDatabase(token);
diff --git a/packages/server/api/src/app/openops-tables/create-table.ts b/packages/server/api/src/app/openops-tables/create-table.ts
index 8fe9cd308d..e02dffb8f7 100644
--- a/packages/server/api/src/app/openops-tables/create-table.ts
+++ b/packages/server/api/src/app/openops-tables/create-table.ts
@@ -1,4 +1,8 @@
-import { createAxiosHeaders, makeOpenOpsTablesPost } from '@openops/common';
+import {
+  axiosTablesSeedRetryConfig,
+  createAxiosHeaders,
+  makeOpenOpsTablesPost,
+} from '@openops/common';
 
 export type Table = {
   id: number;
@@ -23,5 +27,6 @@ export async function createTable(
     `api/database/tables/database/${databaseId}/`,
     requestBody,
     createAxiosHeaders(token),
+    axiosTablesSeedRetryConfig,
   );
 }
diff --git a/packages/server/api/src/app/openops-tables/rename-database.ts b/packages/server/api/src/app/openops-tables/rename-database.ts
index 3f07de7484..201dc23165 100644
--- a/packages/server/api/src/app/openops-tables/rename-database.ts
+++ b/packages/server/api/src/app/openops-tables/rename-database.ts
@@ -1,5 +1,6 @@
 import {
   Application,
+  axiosTablesSeedRetryConfig,
   createAxiosHeaders,
   makeOpenOpsTablesPatch,
 } from '@openops/common';
@@ -18,5 +19,6 @@ export async function renameDatabase(
     `api/applications/${databaseId}/`,
     requestBody,
     createAxiosHeaders(token),
+    axiosTablesSeedRetryConfig,
   );
 }
diff --git a/packages/server/api/src/app/openops-tables/template-tables/create-aggregated-costs-table.ts b/packages/server/api/src/app/openops-tables/template-tables/create-aggregated-costs-table.ts
index a4713487b1..0239a8f099 100644
--- a/packages/server/api/src/app/openops-tables/template-tables/create-aggregated-costs-table.ts
+++ b/packages/server/api/src/app/openops-tables/template-tables/create-aggregated-costs-table.ts
@@ -1,4 +1,5 @@
 import {
+  axiosTablesSeedRetryConfig,
   createAxiosHeaders,
   getFields,
   getPrimaryKeyFieldFromFields,
@@ -47,6 +48,7 @@ export async function addFields(token: string, tableId: number) {
       type: 'text',
     },
     createAxiosHeaders(token),
+    axiosTablesSeedRetryConfig,
   );
   logger.debug(
     `${SEED_LOG_HEADER} After adding primary field ${primaryField.name} with id: ${primaryField.id}`,
@@ -82,6 +84,7 @@ async function addField(
     createFieldEndpoint,
     fieldBody,
     createAxiosHeaders(token),
+    axiosTablesSeedRetryConfig,
   );
 
   logger.debug(
diff --git a/packages/server/api/src/app/openops-tables/template-tables/create-auto-ec2-instances-shutdown-table.ts b/packages/server/api/src/app/openops-tables/template-tables/create-auto-ec2-instances-shutdown-table.ts
index 74218522f6..e85e6883d6 100644
--- a/packages/server/api/src/app/openops-tables/template-tables/create-auto-ec2-instances-shutdown-table.ts
+++ b/packages/server/api/src/app/openops-tables/template-tables/create-auto-ec2-instances-shutdown-table.ts
@@ -1,4 +1,5 @@
 import {
+  axiosTablesSeedRetryConfig,
   createAxiosHeaders,
   getFields,
   getPrimaryKeyFieldFromFields,
@@ -44,6 +45,7 @@ export async function addFields(token: string, tableId: number) {
       type: 'text',
     },
     createAxiosHeaders(token),
+    axiosTablesSeedRetryConfig,
   );
   logger.debug(
     `[Seeding Auto EC2 instances shutdown table] After adding primary field Arn with id: ${primaryField.id}`,
@@ -72,6 +74,7 @@ async function addField(
     createFieldEndpoint,
     fieldBody,
     createAxiosHeaders(token),
+    axiosTablesSeedRetryConfig,
   );
 
   logger.debug(
diff --git a/packages/server/api/src/app/openops-tables/template-tables/create-business-units-table.ts b/packages/server/api/src/app/openops-tables/template-tables/create-business-units-table.ts
index 90a241af97..6492ee1317 100644
--- a/packages/server/api/src/app/openops-tables/template-tables/create-business-units-table.ts
+++ b/packages/server/api/src/app/openops-tables/template-tables/create-business-units-table.ts
@@ -1,4 +1,5 @@
 import {
+  axiosTablesSeedRetryConfig,
   createAxiosHeaders,
   getFields,
   getPrimaryKeyFieldFromFields,
@@ -44,6 +45,7 @@ export async function addFields(token: string, tableId: number) {
       type: 'text',
     },
     createAxiosHeaders(token),
+    axiosTablesSeedRetryConfig,
   );
   logger.debug(
     `[Seeding Business units table] After adding primary field BU name with id: ${primaryField.id}`,
@@ -68,6 +70,7 @@ async function addField(
     createFieldEndpoint,
     fieldBody,
     createAxiosHeaders(token),
+    axiosTablesSeedRetryConfig,
   );
 
   logger.debug(
diff --git a/packages/server/api/src/app/openops-tables/template-tables/create-idle-ebs-volumes-to-delete-table.ts b/packages/server/api/src/app/openops-tables/template-tables/create-idle-ebs-volumes-to-delete-table.ts
index 1cacea66af..815f831a90 100644
--- a/packages/server/api/src/app/openops-tables/template-tables/create-idle-ebs-volumes-to-delete-table.ts
+++ b/packages/server/api/src/app/openops-tables/template-tables/create-idle-ebs-volumes-to-delete-table.ts
@@ -1,4 +1,5 @@
 import {
+  axiosTablesSeedRetryConfig,
   createAxiosHeaders,
   getFields,
   getPrimaryKeyFieldFromFields,
@@ -45,6 +46,7 @@ export async function addFields(token: string, tableId: number) {
       type: 'text',
     },
     createAxiosHeaders(token),
+    axiosTablesSeedRetryConfig,
   );
   logger.debug(
     `[Seeding Idle EBS Volumes to delete table] After adding primary field Arn with id: ${primaryField.id}`,
@@ -90,6 +92,7 @@ async function addField(
     createFieldEndpoint,
     fieldBody,
     createAxiosHeaders(token),
+    axiosTablesSeedRetryConfig,
   );
 
   logger.debug(
diff --git a/packages/server/api/src/app/openops-tables/template-tables/create-opportunities-table.ts b/packages/server/api/src/app/openops-tables/template-tables/create-opportunities-table.ts
index d07542239b..4f4555fccd 100644
--- a/packages/server/api/src/app/openops-tables/template-tables/create-opportunities-table.ts
+++ b/packages/server/api/src/app/openops-tables/template-tables/create-opportunities-table.ts
@@ -1,5 +1,6 @@
 /* eslint-disable @typescript-eslint/no-explicit-any */
 import {
+  axiosTablesSeedRetryConfig,
   createAxiosHeaders,
   getFields,
   getPrimaryKeyFieldFromFields,
@@ -37,6 +38,7 @@ export async function createOpportunitiesTable(
       type: 'uuid',
     },
     createAxiosHeaders(token),
+    axiosTablesSeedRetryConfig,
   );
   logger.debug(
     `[Seeding ${SEED_OPENOPS_TABLE_NAME} table] After adding primary field ID with id: ${primaryField.id}`,
@@ -153,6 +155,7 @@ async function addField(token: string, tableId: number, fieldBody: any) {
     createFieldEndpoint,
     fieldBody,
     createAxiosHeaders(token),
+    axiosTablesSeedRetryConfig,
   );
 
   logger.debug(
diff --git a/packages/server/api/src/app/openops-tables/template-tables/create-resource-bu-tag-assignment-table.ts b/packages/server/api/src/app/openops-tables/template-tables/create-resource-bu-tag-assignment-table.ts
index f7ff22f0a0..b363d4b81d 100644
--- a/packages/server/api/src/app/openops-tables/template-tables/create-resource-bu-tag-assignment-table.ts
+++ b/packages/server/api/src/app/openops-tables/template-tables/create-resource-bu-tag-assignment-table.ts
@@ -1,4 +1,5 @@
 import {
+  axiosTablesSeedRetryConfig,
   createAxiosHeaders,
   getFields,
   getPrimaryKeyFieldFromFields,
@@ -48,6 +49,7 @@ export async function addFields(
       type: 'text',
     },
     createAxiosHeaders(token),
+    axiosTablesSeedRetryConfig,
   );
   logger.debug(
     `[Seeding Resource BU tag assignment table] After adding primary field Resource identifier with id: ${primaryField.id}`,
@@ -93,6 +95,7 @@ async function addField(
     createFieldEndpoint,
     fieldBody,
     createAxiosHeaders(token),
+    axiosTablesSeedRetryConfig,
   );
 
   logger.debug(
diff --git a/packages/server/api/src/app/openops-tables/template-tables/create-tag-owner-mapping-table.ts b/packages/server/api/src/app/openops-tables/template-tables/create-tag-owner-mapping-table.ts
index 376e9950f1..382adc2081 100644
--- a/packages/server/api/src/app/openops-tables/template-tables/create-tag-owner-mapping-table.ts
+++ b/packages/server/api/src/app/openops-tables/template-tables/create-tag-owner-mapping-table.ts
@@ -1,4 +1,5 @@
 import {
+  axiosTablesSeedRetryConfig,
   createAxiosHeaders,
   getFields,
   getPrimaryKeyFieldFromFields,
@@ -49,6 +50,7 @@ export async function addFields(
       type: 'text',
     },
     createAxiosHeaders(token),
+    axiosTablesSeedRetryConfig,
   );
   logger.debug(
     `[Seeding Tag-Owner mapping table] After adding primary field Owner tag value with id: ${primaryField.id}`,
@@ -78,6 +80,7 @@ async function addField(
     createFieldEndpoint,
     fieldBody,
     createAxiosHeaders(token),
+    axiosTablesSeedRetryConfig,
   );
 
   logger.debug(
diff --git a/packages/server/api/test/unit/openops-tables/create-opportunities-table.test.ts b/packages/server/api/test/unit/openops-tables/create-opportunities-table.test.ts
index 17f11f6347..d9f467b777 100644
--- a/packages/server/api/test/unit/openops-tables/create-opportunities-table.test.ts
+++ b/packages/server/api/test/unit/openops-tables/create-opportunities-table.test.ts
@@ -42,6 +42,7 @@ describe('createOpportunityTable', () => {
       'api/database/fields/1/',
       { name: 'ID', type: 'uuid' },
       'some header',
+      expect.any(Object),
     );
 
     const expectedCalls = [
@@ -132,6 +133,7 @@ describe('createOpportunityTable', () => {
         'api/database/fields/table/1/',
         call,
         'some header',
+        expect.any(Object),
       );
     });
   });
diff --git a/packages/server/api/test/unit/openops-tables/create-table.test.ts b/packages/server/api/test/unit/openops-tables/create-table.test.ts
index d689385626..4acdd510bd 100644
--- a/packages/server/api/test/unit/openops-tables/create-table.test.ts
+++ b/packages/server/api/test/unit/openops-tables/create-table.test.ts
@@ -46,6 +46,7 @@ describe('createTable', () => {
         'Content-Type': 'application/json',
         Authorization: `JWT ${token}`,
       }),
+      expect.any(Object),
     );
   });
 });


Reviews:
user: login: cezudas
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T07:37:50Z
user: login: cezudas
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T07:41:57Z
user: login: cezudas
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T07:47:29Z
user: login: cezudas
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T07:49:58Z
user: login: cezudas
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T07:50:13Z
user: login: cezudas
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T07:50:18Z
user: login: cezudas
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T10:27:49Z
user: login: MarceloRGonc
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T15:55:44Z
user: login: cezudas
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T17:45:28Z
user: login: MarceloRGonc
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T19:08:42Z
user: login: cezudas
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-11T06:11:02Z
user: login: cezudas
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-11T06:20:28Z
user: login: cezudas
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-11T07:06:22Z
user: login: cezudas
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-11T07:13:17Z
user: login: cezudas
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-11T07:13:46Z
user: login: bigfluffycookie
body: Looks good, just wait until Marcelo replies to the open comment. 
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-11T07:42:16Z
user: login: MarceloRGonc
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-11T08:13:53Z
user: login: MarceloRGonc
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-11T08:52:34Z
user: login: MarceloRGonc
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-11T08:53:24Z

Review comments:
diff_hunk: @@ -101,6 +101,7 @@ export async function getRows(getRowsParams: GetRowsParams) {
     const getRowsResult = await makeOpenOpsTablesGet<{ results: any[] }[]>(
       url,
       authenticationHeader,
+      false,
path: packages/openops/src/lib/openops-tables/rows.ts
user: login: cezudas
body: I would say it's unlikely this module (used by`openops-tables` block) will be extended/changed more often than the openops-tables seed logic in packages/server/api.
If we don't have reasons to change `rows.ts`, I think it's ok.
created_at: 2025-04-10T07:37:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,42 +1,94 @@
 import { AppSystemProp, system } from '@openops/server-shared';
-import { AxiosHeaders, AxiosResponse, Method } from 'axios';
+import axios, {
+  AxiosHeaders,
+  AxiosInstance,
+  AxiosResponse,
+  Method,
+} from 'axios';
+import axiosRetry from 'axios-retry';
+import { StatusCodes } from 'http-status-codes';
 import { makeHttpRequest } from '../axios-wrapper';
 
+const retryAxiosInstance = axios.create();
+
+axiosRetry(retryAxiosInstance, {
+  retries: 3,
+  retryDelay: (retryCount: number) => {
+    return retryCount * 10000;
+  },
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  retryCondition: (error: any) => {
+    return (
+      error?.response?.status &&
+      error?.response?.status === StatusCodes.CONFLICT
+    );
+  },
+});
+
 export function createAxiosHeaders(token: string): AxiosHeaders {
   return new AxiosHeaders({
     'Content-Type': 'application/json',
     Authorization: `JWT ${token}`,
   });
 }
 
+const getAxiosInstance = (useAxiosRetry: boolean): AxiosInstance => {
+  return useAxiosRetry ? retryAxiosInstance : axios;
+};
+
path: packages/openops/src/lib/openops-tables/requests-helpers.ts
user: login: cezudas
body: It would be convenient always to use axiosRetry with all the "tables" requests.
But it might be a bad idea to use axiosRetry when executing Tables actions.
@rita-gorokhod 
WDYT?
created_at: 2025-04-10T07:41:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,42 +1,94 @@
 import { AppSystemProp, system } from '@openops/server-shared';
-import { AxiosHeaders, AxiosResponse, Method } from 'axios';
+import axios, {
+  AxiosHeaders,
+  AxiosInstance,
+  AxiosResponse,
+  Method,
+} from 'axios';
+import axiosRetry from 'axios-retry';
+import { StatusCodes } from 'http-status-codes';
 import { makeHttpRequest } from '../axios-wrapper';
 
+const retryAxiosInstance = axios.create();
+
+axiosRetry(retryAxiosInstance, {
+  retries: 3,
+  retryDelay: (retryCount: number) => {
+    return retryCount * 10000;
+  },
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  retryCondition: (error: any) => {
+    return (
+      error?.response?.status &&
+      error?.response?.status === StatusCodes.CONFLICT
+    );
+  },
+});
+
 export function createAxiosHeaders(token: string): AxiosHeaders {
   return new AxiosHeaders({
     'Content-Type': 'application/json',
     Authorization: `JWT ${token}`,
   });
 }
 
+const getAxiosInstance = (useAxiosRetry: boolean): AxiosInstance => {
+  return useAxiosRetry ? retryAxiosInstance : axios;
+};
+
 // eslint-disable-next-line @typescript-eslint/no-explicit-any
 export async function makeOpenOpsTablesPost<T>(
   route: string,
   body: any,
   headers?: AxiosHeaders,
+  useAxiosRetry = true,
path: packages/openops/src/lib/openops-tables/requests-helpers.ts
user: login: cezudas
body: Not ideal, yet it avoids a more significant refactor = big PR. 
It makes this assumption:
https://github.com/openops-cloud/openops/pull/429/files#diff-a9edf8d5509b4c4d58a0aeed27230fa9278754f16e14c77fe3dc14f864cb1d91R104
created_at: 2025-04-10T07:47:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -114,6 +115,7 @@ export async function updateRow(updateRowParams: UpdateRowParams) {
       `api/database/rows/table/${updateRowParams.tableId}/${updateRowParams.rowId}/?user_field_names=true`,
       updateRowParams.fields,
       authenticationHeader,
+      false,
path: packages/openops/src/lib/openops-tables/rows.ts
user: login: cezudas
body: same remark as here https://github.com/openops-cloud/openops/pull/429/files#diff-a9edf8d5509b4c4d58a0aeed27230fa9278754f16e14c77fe3dc14f864cb1d91R104
created_at: 2025-04-10T07:49:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -125,6 +127,7 @@ export async function addRow(addRowParams: AddRowParams) {
       `api/database/rows/table/${addRowParams.tableId}/?user_field_names=true`,
       addRowParams.fields,
       authenticationHeader,
+      false,
path: packages/openops/src/lib/openops-tables/rows.ts
user: login: cezudas
body: same remark as here https://github.com/openops-cloud/openops/pull/429/files#diff-a9edf8d5509b4c4d58a0aeed27230fa9278754f16e14c77fe3dc14f864cb1d91R104
created_at: 2025-04-10T07:50:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -135,6 +138,7 @@ export async function deleteRow(deleteRowParams: DeleteRowParams) {
     return await makeOpenOpsTablesDelete(
       `api/database/rows/table/${deleteRowParams.tableId}/${deleteRowParams.rowId}/`,
       authenticationHeader,
+      false,
path: packages/openops/src/lib/openops-tables/rows.ts
user: login: cezudas
body: same remark as here https://github.com/openops-cloud/openops/pull/429/files#diff-a9edf8d5509b4c4d58a0aeed27230fa9278754f16e14c77fe3dc14f864cb1d91R104
created_at: 2025-04-10T07:50:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -7,10 +7,10 @@ import {
 } from '../openops-tables/filters';
 import {
   createAxiosHeaders,
-  makeOpenOpsTablesDelete,
-  makeOpenOpsTablesGet,
-  makeOpenOpsTablesPatch,
-  makeOpenOpsTablesPost,
+  makeOpenOpsTablesDeleteWithoutRetry,
+  makeOpenOpsTablesGetWithoutRetry,
+  makeOpenOpsTablesPatchWithoutRetry,
+  makeOpenOpsTablesPostWithoutRetry,
 } from '../openops-tables/requests-helpers';
path: packages/openops/src/lib/openops-tables/rows.ts
user: login: cezudas
body: it assumes the actions of table block should not use axios retry -> use retry only for the all tables requests fired during app seeding
created_at: 2025-04-10T10:27:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,28 +1,93 @@
 import { AppSystemProp, system } from '@openops/server-shared';
-import { AxiosHeaders, AxiosResponse, Method } from 'axios';
+import axios, {
+  AxiosHeaders,
+  AxiosInstance,
+  AxiosResponse,
+  Method,
+} from 'axios';
+import axiosRetry from 'axios-retry';
+import { StatusCodes } from 'http-status-codes';
 import { makeHttpRequest } from '../axios-wrapper';
 
+const retryAxiosInstance = axios.create();
+
+axiosRetry(retryAxiosInstance, {
+  retries: 3,
+  retryDelay: (retryCount: number) => {
+    return retryCount * 10000;
+  },
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  retryCondition: (error: any) => {
+    return (
+      error?.response?.status &&
+      error?.response?.status === StatusCodes.CONFLICT
+    );
+  },
+});
+
+const standardAxiosInstance = axios;
+
 export function createAxiosHeaders(token: string): AxiosHeaders {
   return new AxiosHeaders({
     'Content-Type': 'application/json',
     Authorization: `JWT ${token}`,
   });
 }
 
-// eslint-disable-next-line @typescript-eslint/no-explicit-any
 export async function makeOpenOpsTablesPost<T>(
   route: string,
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
path: packages/openops/src/lib/openops-tables/requests-helpers.ts
user: login: bigfluffycookie
body: Maybe just disable for the whole file
created_at: 2025-04-10T15:21:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -15,8 +22,13 @@ export async function makeHttpRequest<T>(
       url,
     };
 
-    const response = await axios(config);
-
+    // eslint-disable-next-line @typescript-eslint/no-explicit-any
+    let response: AxiosResponse<any, any>;
path: packages/openops/src/lib/axios-wrapper.ts
user: login: MarceloRGonc
body:     let response: AxiosResponse<T, AxiosRequestConfig>;
created_at: 2025-04-10T15:45:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,42 +1,94 @@
 import { AppSystemProp, system } from '@openops/server-shared';
-import { AxiosHeaders, AxiosResponse, Method } from 'axios';
+import axios, {
+  AxiosHeaders,
+  AxiosInstance,
+  AxiosResponse,
+  Method,
+} from 'axios';
+import axiosRetry from 'axios-retry';
+import { StatusCodes } from 'http-status-codes';
 import { makeHttpRequest } from '../axios-wrapper';
 
+const retryAxiosInstance = axios.create();
+
+axiosRetry(retryAxiosInstance, {
+  retries: 3,
+  retryDelay: (retryCount: number) => {
+    return retryCount * 10000;
+  },
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  retryCondition: (error: any) => {
+    return (
+      error?.response?.status &&
+      error?.response?.status === StatusCodes.CONFLICT
+    );
path: packages/openops/src/lib/openops-tables/requests-helpers.ts
user: login: MarceloRGonc
body: This retry condition must be configurable. In the seed we are using this condition but we may want to use another one in other requests.
created_at: 2025-04-10T15:47:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,28 +1,93 @@
 import { AppSystemProp, system } from '@openops/server-shared';
-import { AxiosHeaders, AxiosResponse, Method } from 'axios';
+import axios, {
+  AxiosHeaders,
+  AxiosInstance,
+  AxiosResponse,
+  Method,
+} from 'axios';
+import axiosRetry from 'axios-retry';
+import { StatusCodes } from 'http-status-codes';
 import { makeHttpRequest } from '../axios-wrapper';
 
+const retryAxiosInstance = axios.create();
+
+axiosRetry(retryAxiosInstance, {
+  retries: 3,
+  retryDelay: (retryCount: number) => {
+    return retryCount * 10000;
+  },
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  retryCondition: (error: any) => {
+    return (
+      error?.response?.status &&
+      error?.response?.status === StatusCodes.CONFLICT
+    );
+  },
+});
+
+const standardAxiosInstance = axios;
+
 export function createAxiosHeaders(token: string): AxiosHeaders {
   return new AxiosHeaders({
     'Content-Type': 'application/json',
     Authorization: `JWT ${token}`,
   });
 }
 
-// eslint-disable-next-line @typescript-eslint/no-explicit-any
 export async function makeOpenOpsTablesPost<T>(
   route: string,
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  body: any,
+  headers?: AxiosHeaders,
+): Promise<T> {
+  return makeOpenOpsTablesRequest<T>(
+    'POST',
+    route,
+    retryAxiosInstance,
+    body,
+    headers,
+  );
+}
+
+export async function makeOpenOpsTablesPostWithoutRetry<T>(
+  route: string,
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
   body: any,
   headers?: AxiosHeaders,
 ): Promise<T> {
-  return makeOpenOpsTablesRequest<T>('POST', route, body, headers);
+  return makeOpenOpsTablesRequest<T>(
+    'POST',
+    route,
+    standardAxiosInstance,
+    body,
+    headers,
+  );
 }
path: packages/openops/src/lib/openops-tables/requests-helpers.ts
user: login: MarceloRGonc
body: Do not create a new method. Add an optional parameter in the previous one:
```
retryConfigs?: {
  retries: number, // default 3
  retryCondition: fn: (error: any) => boolean,
  retryDelay: (retryCount: number) => number, // some default strategy
}```
created_at: 2025-04-10T15:51:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,28 +1,93 @@
 import { AppSystemProp, system } from '@openops/server-shared';
-import { AxiosHeaders, AxiosResponse, Method } from 'axios';
+import axios, {
+  AxiosHeaders,
+  AxiosInstance,
+  AxiosResponse,
+  Method,
+} from 'axios';
+import axiosRetry from 'axios-retry';
+import { StatusCodes } from 'http-status-codes';
 import { makeHttpRequest } from '../axios-wrapper';
 
+const retryAxiosInstance = axios.create();
+
+axiosRetry(retryAxiosInstance, {
path: packages/openops/src/lib/openops-tables/requests-helpers.ts
user: login: MarceloRGonc
body: Move this method to the axios-wrapper file.
created_at: 2025-04-10T15:53:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,11 +1,18 @@
 import { logger } from '@openops/server-shared';
-import axios, { AxiosError, AxiosHeaders, Method } from 'axios';
+import axios, {
+  AxiosError,
+  AxiosHeaders,
+  AxiosInstance,
+  AxiosResponse,
+  Method,
+} from 'axios';
 
 export async function makeHttpRequest<T>(
   method: Method,
   url: string,
   headers?: AxiosHeaders,
   body?: any,
+  axiosInstance?: AxiosInstance,
path: packages/openops/src/lib/axios-wrapper.ts
user: login: MarceloRGonc
body: Change this to something like:

```
retryConfigs?: {
  retries: number, // default 3
  retryCondition: fn: (error: any) => boolean,
  retryDelay: (retryCount: number) => number, // some default strategy
}
```

If defined create the axios instance with the retries.
created_at: 2025-04-10T15:55:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,22 +1,39 @@
 import { logger } from '@openops/server-shared';
 import axios, { AxiosError, AxiosHeaders, Method } from 'axios';
+import axiosRetry, { IAxiosRetryConfig } from 'axios-retry';
+
+const standardAxiosInstance = axios;
+
+let retryAxiosInstance: ReturnType<typeof axios.create> | null = null;
+
+function getRetryAxiosInstance(retryConfigs: IAxiosRetryConfig) {
path: packages/openops/src/lib/axios-wrapper.ts
user: login: cezudas
body: I wanted to have it as top level code (without any function)
```
const retryAxiosInstance = axios.create();
axiosRetry(retryAxiosInstance, { ... });
```

But I just couldn't get `axios-wrapper.test.ts` to pass because the could would run before your test file can mock axios.create(), so the create() call returns an incomplete mock, and axiosRetry() tries to access .interceptors on it → crash.


So this "Lazy Singleton for Retry Instance" is a compromise, at least I don't create instances every time
created_at: 2025-04-10T17:45:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,22 +1,39 @@
 import { logger } from '@openops/server-shared';
 import axios, { AxiosError, AxiosHeaders, Method } from 'axios';
+import axiosRetry, { IAxiosRetryConfig } from 'axios-retry';
+
+const standardAxiosInstance = axios;
+
+let retryAxiosInstance: ReturnType<typeof axios.create> | null = null;
+
+function getRetryAxiosInstance(retryConfigs: IAxiosRetryConfig) {
path: packages/openops/src/lib/axios-wrapper.ts
user: login: MarceloRGonc
body: This does not work. If the second call has different configs you will use the old instance
created_at: 2025-04-10T18:58:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -44,7 +49,11 @@ export async function authenticateDefaultUserInOpenOpsTables(): Promise<AuthToke
     const email = system.getOrThrow(AppSystemProp.OPENOPS_ADMIN_EMAIL);
     const password = system.getOrThrow(AppSystemProp.OPENOPS_ADMIN_PASSWORD);
 
-    tokens = await authenticateUserInOpenOpsTables(email, password);
+    tokens = await authenticateUserInOpenOpsTables(
+      email,
+      password,
+      axiosTablesSeedRetryConfig,
+    );
path: packages/openops/src/lib/openops-tables/auth-user.ts
user: login: MarceloRGonc
body: Why we need to retry?
created_at: 2025-04-10T19:02:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -32,6 +36,7 @@ export async function authenticateUserInOpenOpsTables(
     'api/user/token-auth/',
     requestBody,
     headers,
+    axiosRetryConfig,
path: packages/openops/src/lib/openops-tables/auth-user.ts
user: login: MarceloRGonc
body: Why we need to retry?
created_at: 2025-04-10T19:03:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -9,34 +11,73 @@ export function createAxiosHeaders(token: string): AxiosHeaders {
   });
 }
 
+export const axiosTablesSeedRetryConfig: IAxiosRetryConfig = {
+  retries: 3,
+  retryDelay: (retryCount: number) => {
+    return retryCount * 10000;
path: packages/openops/src/lib/openops-tables/requests-helpers.ts
user: login: MarceloRGonc
body: Isn't that a long time?
10s 20s 30s
created_at: 2025-04-10T19:08:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -12,6 +13,7 @@ export async function getFields(
   const fields = await makeOpenOpsTablesGet<any[]>(
     `api/database/fields/table/${tableId}/?user_field_names=${userFieldNames}`,
     authenticationHeader,
+    axiosTablesSeedRetryConfig,
path: packages/openops/src/lib/openops-tables/fields.ts
user: login: MarceloRGonc
body: Why we need to retry?
created_at: 2025-04-10T19:08:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -13,6 +17,7 @@ export async function getDefaultDatabaseId(
     await makeOpenOpsTablesGet<Application>(
       `api/applications/`,
       authenticationHeader,
+      axiosTablesSeedRetryConfig,
path: packages/openops/src/lib/openops-tables/applications-service.ts
user: login: MarceloRGonc
body: Why we need to retry?
created_at: 2025-04-10T19:08:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -13,6 +17,7 @@ export async function getDefaultDatabaseId(
     await makeOpenOpsTablesGet<Application>(
       `api/applications/`,
       authenticationHeader,
+      axiosTablesSeedRetryConfig,
path: packages/openops/src/lib/openops-tables/applications-service.ts
user: login: cezudas
body: it's technically an API call to tables service and `getDefaultDatabaseId` is only called from `packages/server/api/src/app/database/seeds/openops-tables-rename-database.ts`. I could add optional axiosRetryConfig parameter to getDefaultDatabaseId and  pass axiosTablesSeedRetryConfig  from `packages/server/api/src/app/database/seeds/openops-tables-rename-database.ts`. Let me know
created_at: 2025-04-11T06:11:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -32,6 +36,7 @@ export async function authenticateUserInOpenOpsTables(
     'api/user/token-auth/',
     requestBody,
     headers,
+    axiosRetryConfig,
path: packages/openops/src/lib/openops-tables/auth-user.ts
user: login: cezudas
body: There is a `authenticateDefaultUserInOpenOpsTables` function in `packages/openops/src/lib/openops-tables/auth-user.ts`. Following function calls we have seed code using it:
packages/server/api/src/app/database/seeds/openops-opportunities-table-seed.ts
packages/server/api/src/app/database/seeds/openops-opportunities-table-seed.ts
packages/server/api/src/app/database/seeds/openops-delete-old-opportunities-table.ts
created_at: 2025-04-11T06:20:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,22 +1,39 @@
 import { logger } from '@openops/server-shared';
 import axios, { AxiosError, AxiosHeaders, Method } from 'axios';
+import axiosRetry, { IAxiosRetryConfig } from 'axios-retry';
+
+const standardAxiosInstance = axios;
+
+let retryAxiosInstance: ReturnType<typeof axios.create> | null = null;
+
+function getRetryAxiosInstance(retryConfigs: IAxiosRetryConfig) {
path: packages/openops/src/lib/axios-wrapper.ts
user: login: cezudas
body: Code-wise it would be simple to create an instance for every tables request but it has implications 🚫 
- Performance hit: Creating an instance allocates memory and processes configuration each time, which adds overhead in high-frequency or concurrent calls.
- No caching or connection reuse: Recreating instance means not reusing connection pools or interceptors.

I've also considered creating a Pool/Cache of Instances per Retry Config but IAxiosRetryConfig interface definition has functions and I can't think of a reliable way since both hashing and deep comparison will be unreliable.
WDYT? @MarceloRGonc 
created_at: 2025-04-11T07:06:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -12,6 +13,7 @@ export async function getFields(
   const fields = await makeOpenOpsTablesGet<any[]>(
     `api/database/fields/table/${tableId}/?user_field_names=${userFieldNames}`,
     authenticationHeader,
+    axiosTablesSeedRetryConfig,
path: packages/openops/src/lib/openops-tables/fields.ts
user: login: cezudas
body: I added axiosRetryConfig?: IAxiosRetryConfig param to getFields function in the meantime. So the seed logic call can pass axiosTablesSeedRetryConfig and tables block code can skip retry
created_at: 2025-04-11T07:13:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -9,34 +11,73 @@ export function createAxiosHeaders(token: string): AxiosHeaders {
   });
 }
 
+export const axiosTablesSeedRetryConfig: IAxiosRetryConfig = {
+  retries: 3,
+  retryDelay: (retryCount: number) => {
+    return retryCount * 10000;
path: packages/openops/src/lib/openops-tables/requests-helpers.ts
user: login: cezudas
body: Alright, I've reduced it by a factor of 10: 1s 2s 3s
created_at: 2025-04-11T07:13:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,22 +1,39 @@
 import { logger } from '@openops/server-shared';
 import axios, { AxiosError, AxiosHeaders, Method } from 'axios';
+import axiosRetry, { IAxiosRetryConfig } from 'axios-retry';
+
+const standardAxiosInstance = axios;
+
+let retryAxiosInstance: ReturnType<typeof axios.create> | null = null;
+
+function getRetryAxiosInstance(retryConfigs: IAxiosRetryConfig) {
path: packages/openops/src/lib/axios-wrapper.ts
user: login: MarceloRGonc
body: For now, I think we should keep it simple. And create a new instance for every request with retries. Performance won't be affected much and we keep the method reusable. If we notice performance issues, we adopt another strategy, but it's not worth the effort at this point.
created_at: 2025-04-11T08:11:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -9,34 +11,73 @@ export function createAxiosHeaders(token: string): AxiosHeaders {
   });
 }
 
+export const axiosTablesSeedRetryConfig: IAxiosRetryConfig = {
+  retries: 3,
+  retryDelay: (retryCount: number) => {
+    return retryCount * 1000;
path: packages/openops/src/lib/openops-tables/requests-helpers.ts
user: login: MarceloRGonc
body: Add a debug log, to understand if we are doing too many retries
created_at: 2025-04-11T08:13:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,22 +1,36 @@
 import { logger } from '@openops/server-shared';
 import axios, { AxiosError, AxiosHeaders, Method } from 'axios';
+import axiosRetry, { IAxiosRetryConfig } from 'axios-retry';
+
+const standardAxiosInstance = axios;
+
+function getRetryAxiosInstance(retryConfigs: IAxiosRetryConfig) {
+  const retryAxiosInstance = axios.create();
+
+  axiosRetry(retryAxiosInstance, retryConfigs);
+  return retryAxiosInstance;
+}
 
 export async function makeHttpRequest<T>(
   method: Method,
   url: string,
   headers?: AxiosHeaders,
   body?: any,
+  retryConfigs?: IAxiosRetryConfig,
 ): Promise<T> {
   try {
     const config = {
-      data: body,
-      headers,
       method,
       url,
+      headers,
+      data: body,
     };
 
-    const response = await axios(config);
+    const axiosInstance = retryConfigs
+      ? getRetryAxiosInstance(retryConfigs)
+      : standardAxiosInstance;
path: packages/openops/src/lib/axios-wrapper.ts
user: login: MarceloRGonc
body: ```suggestion
      : axios;
```
created_at: 2025-04-11T08:51:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,22 +1,36 @@
 import { logger } from '@openops/server-shared';
 import axios, { AxiosError, AxiosHeaders, Method } from 'axios';
+import axiosRetry, { IAxiosRetryConfig } from 'axios-retry';
+
+const standardAxiosInstance = axios;
path: packages/openops/src/lib/axios-wrapper.ts
user: login: MarceloRGonc
body: You don't need this variable
created_at: 2025-04-11T08:52:00Z
author_association: CONTRIBUTOR

PR comments:
user: login: linear[bot]
created_at: 2025-04-10T07:29:46Z
body: <p><a href="https://linear.app/openops/issue/OPS-1311/seed-tables-throw-an-error-that-the-column-is-already-in-use">OPS-1311 Seed tables throw an error that the column is already in use</a></p>
author_association: NONE
user: login: sonarqubecloud[bot]
created_at: 2025-04-11T08:57:44Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=openops-cloud_openops&pullRequest=429) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=openops-cloud_openops&pullRequest=429&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=openops-cloud_openops&pullRequest=429&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=openops-cloud_openops&pullRequest=429&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=openops-cloud_openops&pullRequest=429&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [4.4% Duplication on New Code](https://sonarcloud.io/component_measures?id=openops-cloud_openops&pullRequest=429&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=openops-cloud_openops&pullRequest=429)


author_association: NONE