Add retry + idempotency on trading when try + add more tts
This commit is contained in:
2
scripts/build_and_run.sh
Normal file → Executable file
2
scripts/build_and_run.sh
Normal file → Executable file
@@ -1,7 +1,7 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
# Navigate to the src directory
|
# Navigate to the src directory
|
||||||
cd ../src
|
cd src
|
||||||
|
|
||||||
# Build the managing.api image (now includes all workers as background services)
|
# Build the managing.api image (now includes all workers as background services)
|
||||||
docker build -t managing.api -f Managing.Api/Dockerfile . --no-cache
|
docker build -t managing.api -f Managing.Api/Dockerfile . --no-cache
|
||||||
|
|||||||
@@ -32,6 +32,8 @@
|
|||||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="6.6.1"/>
|
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="6.6.1"/>
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="6.6.1"/>
|
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="6.6.1"/>
|
||||||
<PackageReference Include="xunit" Version="2.8.0"/>
|
<PackageReference Include="xunit" Version="2.8.0"/>
|
||||||
|
<PackageReference Include="Polly" Version="8.4.0"/>
|
||||||
|
<PackageReference Include="Polly.Extensions.Http" Version="3.0.0"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -13,7 +13,10 @@
|
|||||||
"AppSecret": "3STq1UyPJ5WHixArBcVBKecWtyR4QpgZ1uju4HHvvJH2RwtacJnvoyzuaiNC8Xibi4rQb3eeH2YtncKrMxCYiV3a"
|
"AppSecret": "3STq1UyPJ5WHixArBcVBKecWtyR4QpgZ1uju4HHvvJH2RwtacJnvoyzuaiNC8Xibi4rQb3eeH2YtncKrMxCYiV3a"
|
||||||
},
|
},
|
||||||
"Web3Proxy": {
|
"Web3Proxy": {
|
||||||
"BaseUrl": "http://srv-captain--web3-proxy:4111"
|
"BaseUrl": "http://srv-captain--web3-proxy:4111",
|
||||||
|
"MaxRetryAttempts": 3,
|
||||||
|
"RetryDelayMs": 1000,
|
||||||
|
"TimeoutSeconds": 30
|
||||||
},
|
},
|
||||||
"Serilog": {
|
"Serilog": {
|
||||||
"MinimumLevel": {
|
"MinimumLevel": {
|
||||||
|
|||||||
@@ -18,7 +18,10 @@
|
|||||||
"RefundEndpoint": "/api/credits/refund"
|
"RefundEndpoint": "/api/credits/refund"
|
||||||
},
|
},
|
||||||
"Web3Proxy": {
|
"Web3Proxy": {
|
||||||
"BaseUrl": "http://srv-captain--web3-proxy:4111"
|
"BaseUrl": "http://srv-captain--web3-proxy:4111",
|
||||||
|
"MaxRetryAttempts": 3,
|
||||||
|
"RetryDelayMs": 1000,
|
||||||
|
"TimeoutSeconds": 30
|
||||||
},
|
},
|
||||||
"Serilog": {
|
"Serilog": {
|
||||||
"MinimumLevel": {
|
"MinimumLevel": {
|
||||||
|
|||||||
@@ -21,7 +21,10 @@
|
|||||||
"Token": ""
|
"Token": ""
|
||||||
},
|
},
|
||||||
"Web3Proxy": {
|
"Web3Proxy": {
|
||||||
"BaseUrl": "http://localhost:4111"
|
"BaseUrl": "http://localhost:4111",
|
||||||
|
"MaxRetryAttempts": 3,
|
||||||
|
"RetryDelayMs": 1000,
|
||||||
|
"TimeoutSeconds": 30
|
||||||
},
|
},
|
||||||
"Kaigen": {
|
"Kaigen": {
|
||||||
"BaseUrl": "https://kaigen-back-development.up.railway.app",
|
"BaseUrl": "https://kaigen-back-development.up.railway.app",
|
||||||
|
|||||||
@@ -45,3 +45,24 @@ services:
|
|||||||
- 8086:8086
|
- 8086:8086
|
||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:8.0.3
|
||||||
|
ports:
|
||||||
|
- "6379:6379"
|
||||||
|
volumes:
|
||||||
|
- redis_data:/data
|
||||||
|
networks:
|
||||||
|
- managing-network
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- REDIS_PASSWORD=SuperSecretPassword
|
||||||
|
command: >
|
||||||
|
sh -c "
|
||||||
|
if [ -n \"$$REDIS_PASSWORD\" ]; then
|
||||||
|
redis-server --appendonly yes --requirepass $$REDIS_PASSWORD
|
||||||
|
redis-cli -a $$REDIS_PASSWORD
|
||||||
|
else
|
||||||
|
redis-server --appendonly yes
|
||||||
|
redis-cli
|
||||||
|
fi
|
||||||
|
"
|
||||||
@@ -20,9 +20,17 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- managing-network
|
- managing-network
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:8.0.3
|
||||||
|
volumes:
|
||||||
|
- redis_data:/data
|
||||||
|
networks:
|
||||||
|
- managing-network
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
influxdata: {}
|
influxdata: {}
|
||||||
postgresdata: {}
|
postgresdata: {}
|
||||||
|
redis_data: {}
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
managing-network:
|
managing-network:
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Collections;
|
using System.Collections;
|
||||||
|
using System.Net;
|
||||||
using System.Net.Http.Json;
|
using System.Net.Http.Json;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
@@ -6,7 +7,9 @@ using System.Web;
|
|||||||
using Managing.Application.Abstractions.Services;
|
using Managing.Application.Abstractions.Services;
|
||||||
using Managing.Domain.Accounts;
|
using Managing.Domain.Accounts;
|
||||||
using Managing.Infrastructure.Evm.Models.Proxy;
|
using Managing.Infrastructure.Evm.Models.Proxy;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
using Polly;
|
||||||
using static Managing.Common.Enums;
|
using static Managing.Common.Enums;
|
||||||
|
|
||||||
namespace Managing.Infrastructure.Evm.Services
|
namespace Managing.Infrastructure.Evm.Services
|
||||||
@@ -14,6 +17,9 @@ namespace Managing.Infrastructure.Evm.Services
|
|||||||
public class Web3ProxySettings
|
public class Web3ProxySettings
|
||||||
{
|
{
|
||||||
public string BaseUrl { get; set; } = "http://localhost:3000";
|
public string BaseUrl { get; set; } = "http://localhost:3000";
|
||||||
|
public int MaxRetryAttempts { get; set; } = 3;
|
||||||
|
public int RetryDelayMs { get; set; } = 1000;
|
||||||
|
public int TimeoutSeconds { get; set; } = 30;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class Web3ProxyService : IWeb3ProxyService
|
public class Web3ProxyService : IWeb3ProxyService
|
||||||
@@ -21,15 +27,98 @@ namespace Managing.Infrastructure.Evm.Services
|
|||||||
private readonly HttpClient _httpClient;
|
private readonly HttpClient _httpClient;
|
||||||
private readonly Web3ProxySettings _settings;
|
private readonly Web3ProxySettings _settings;
|
||||||
private readonly JsonSerializerOptions _jsonOptions;
|
private readonly JsonSerializerOptions _jsonOptions;
|
||||||
|
private readonly IAsyncPolicy<HttpResponseMessage> _retryPolicy;
|
||||||
|
private readonly ILogger<Web3ProxyService> _logger;
|
||||||
|
|
||||||
public Web3ProxyService(IOptions<Web3ProxySettings> options)
|
public Web3ProxyService(IOptions<Web3ProxySettings> options, ILogger<Web3ProxyService> logger)
|
||||||
{
|
{
|
||||||
_httpClient = new HttpClient();
|
|
||||||
_settings = options.Value;
|
_settings = options.Value;
|
||||||
|
_logger = logger;
|
||||||
|
|
||||||
|
_httpClient = new HttpClient();
|
||||||
|
_httpClient.Timeout = TimeSpan.FromSeconds(_settings.TimeoutSeconds);
|
||||||
|
|
||||||
_jsonOptions = new JsonSerializerOptions
|
_jsonOptions = new JsonSerializerOptions
|
||||||
{
|
{
|
||||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Configure retry policy
|
||||||
|
_retryPolicy = Policy
|
||||||
|
.Handle<HttpRequestException>()
|
||||||
|
.Or<TaskCanceledException>()
|
||||||
|
.Or<TimeoutException>()
|
||||||
|
.OrResult<HttpResponseMessage>(r => !r.IsSuccessStatusCode && IsRetryableStatusCode(r.StatusCode))
|
||||||
|
.WaitAndRetryAsync(
|
||||||
|
retryCount: _settings.MaxRetryAttempts,
|
||||||
|
sleepDurationProvider: retryAttempt => TimeSpan.FromMilliseconds(
|
||||||
|
_settings.RetryDelayMs * Math.Pow(2, retryAttempt - 1) + // Exponential backoff
|
||||||
|
new Random().Next(0, _settings.RetryDelayMs / 4) // Add jitter
|
||||||
|
),
|
||||||
|
onRetry: (outcome, timespan, retryCount, context) =>
|
||||||
|
{
|
||||||
|
var exception = outcome.Exception;
|
||||||
|
var response = outcome.Result;
|
||||||
|
var errorMessage = exception?.Message ?? $"HTTP {response?.StatusCode}";
|
||||||
|
|
||||||
|
_logger.LogWarning(
|
||||||
|
"Web3Proxy request failed (attempt {RetryCount}/{MaxRetries}): {Error}. Retrying in {Delay}ms",
|
||||||
|
retryCount, _settings.MaxRetryAttempts + 1, errorMessage, timespan.TotalMilliseconds);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsRetryableStatusCode(HttpStatusCode statusCode)
|
||||||
|
{
|
||||||
|
return statusCode == HttpStatusCode.RequestTimeout ||
|
||||||
|
statusCode == HttpStatusCode.TooManyRequests ||
|
||||||
|
statusCode == HttpStatusCode.InternalServerError ||
|
||||||
|
statusCode == HttpStatusCode.BadGateway ||
|
||||||
|
statusCode == HttpStatusCode.ServiceUnavailable ||
|
||||||
|
statusCode == HttpStatusCode.GatewayTimeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<T> ExecuteWithRetryAsync<T>(Func<Task<HttpResponseMessage>> httpCall, string operationName)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var response = await _retryPolicy.ExecuteAsync(httpCall);
|
||||||
|
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
await HandleErrorResponse(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = await response.Content.ReadFromJsonAsync<T>(_jsonOptions);
|
||||||
|
return result ?? throw new Web3ProxyException($"Failed to deserialize response for {operationName}");
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (!(ex is Web3ProxyException))
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Operation {OperationName} failed after all retry attempts", operationName);
|
||||||
|
SentrySdk.CaptureException(ex);
|
||||||
|
throw new Web3ProxyException($"Failed to execute {operationName}: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<T> ExecuteWithRetryAsync<T>(Func<Task<HttpResponseMessage>> httpCall, string operationName, string idempotencyKey)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var response = await _retryPolicy.ExecuteAsync(httpCall);
|
||||||
|
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
await HandleErrorResponse(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = await response.Content.ReadFromJsonAsync<T>(_jsonOptions);
|
||||||
|
return result ?? throw new Web3ProxyException($"Failed to deserialize response for {operationName}");
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (!(ex is Web3ProxyException))
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Operation {OperationName} failed after all retry attempts (IdempotencyKey: {IdempotencyKey})", operationName, idempotencyKey);
|
||||||
|
SentrySdk.CaptureException(ex);
|
||||||
|
throw new Web3ProxyException($"Failed to execute {operationName}: {ex.Message}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<T> CallPrivyServiceAsync<T>(string endpoint, object payload)
|
public async Task<T> CallPrivyServiceAsync<T>(string endpoint, object payload)
|
||||||
@@ -40,26 +129,22 @@ namespace Managing.Infrastructure.Evm.Services
|
|||||||
}
|
}
|
||||||
|
|
||||||
var url = $"{_settings.BaseUrl}/api/privy{endpoint}";
|
var url = $"{_settings.BaseUrl}/api/privy{endpoint}";
|
||||||
|
var idempotencyKey = Guid.NewGuid().ToString();
|
||||||
|
|
||||||
try
|
return await ExecuteWithRetryAsync<T>(
|
||||||
{
|
() => {
|
||||||
var response = await _httpClient.PostAsJsonAsync(url, payload, _jsonOptions);
|
var request = new HttpRequestMessage(HttpMethod.Post, url)
|
||||||
|
{
|
||||||
if (!response.IsSuccessStatusCode)
|
Content = JsonContent.Create(payload, options: _jsonOptions)
|
||||||
{
|
};
|
||||||
await HandleErrorResponse(response);
|
request.Headers.Add("Idempotency-Key", idempotencyKey);
|
||||||
}
|
return _httpClient.SendAsync(request);
|
||||||
|
},
|
||||||
return await response.Content.ReadFromJsonAsync<T>(_jsonOptions);
|
$"CallPrivyServiceAsync({endpoint})",
|
||||||
}
|
idempotencyKey);
|
||||||
catch (Exception ex) when (!(ex is Web3ProxyException))
|
|
||||||
{
|
|
||||||
SentrySdk.CaptureException(ex);
|
|
||||||
throw new Web3ProxyException($"Failed to call Privy service at {endpoint}: {ex.Message}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<T> GetPrivyServiceAsync<T>(string endpoint, object payload = null)
|
public async Task<T> GetPrivyServiceAsync<T>(string endpoint, object? payload = null)
|
||||||
{
|
{
|
||||||
if (!endpoint.StartsWith("/"))
|
if (!endpoint.StartsWith("/"))
|
||||||
{
|
{
|
||||||
@@ -73,22 +158,9 @@ namespace Managing.Infrastructure.Evm.Services
|
|||||||
url += BuildQueryString(payload);
|
url += BuildQueryString(payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
return await ExecuteWithRetryAsync<T>(
|
||||||
{
|
() => _httpClient.GetAsync(url),
|
||||||
var response = await _httpClient.GetAsync(url);
|
$"GetPrivyServiceAsync({endpoint})");
|
||||||
|
|
||||||
if (!response.IsSuccessStatusCode)
|
|
||||||
{
|
|
||||||
await HandleErrorResponse(response);
|
|
||||||
}
|
|
||||||
|
|
||||||
return await response.Content.ReadFromJsonAsync<T>(_jsonOptions);
|
|
||||||
}
|
|
||||||
catch (Exception ex) when (!(ex is Web3ProxyException))
|
|
||||||
{
|
|
||||||
SentrySdk.CaptureException(ex);
|
|
||||||
throw new Web3ProxyException($"Failed to get Privy service at {endpoint}: {ex.Message}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<T> CallGmxServiceAsync<T>(string endpoint, object payload)
|
public async Task<T> CallGmxServiceAsync<T>(string endpoint, object payload)
|
||||||
@@ -99,26 +171,22 @@ namespace Managing.Infrastructure.Evm.Services
|
|||||||
}
|
}
|
||||||
|
|
||||||
var url = $"{_settings.BaseUrl}/api/gmx{endpoint}";
|
var url = $"{_settings.BaseUrl}/api/gmx{endpoint}";
|
||||||
|
var idempotencyKey = Guid.NewGuid().ToString();
|
||||||
|
|
||||||
try
|
return await ExecuteWithRetryAsync<T>(
|
||||||
{
|
() => {
|
||||||
var response = await _httpClient.PostAsJsonAsync(url, payload, _jsonOptions);
|
var request = new HttpRequestMessage(HttpMethod.Post, url)
|
||||||
|
{
|
||||||
if (!response.IsSuccessStatusCode)
|
Content = JsonContent.Create(payload, options: _jsonOptions)
|
||||||
{
|
};
|
||||||
await HandleErrorResponse(response);
|
request.Headers.Add("Idempotency-Key", idempotencyKey);
|
||||||
}
|
return _httpClient.SendAsync(request);
|
||||||
|
},
|
||||||
return await response.Content.ReadFromJsonAsync<T>(_jsonOptions);
|
$"CallGmxServiceAsync({endpoint})",
|
||||||
}
|
idempotencyKey);
|
||||||
catch (Exception ex) when (!(ex is Web3ProxyException))
|
|
||||||
{
|
|
||||||
SentrySdk.CaptureException(ex);
|
|
||||||
throw new Web3ProxyException($"Failed to call GMX service at {endpoint}: {ex.Message}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<T> GetGmxServiceAsync<T>(string endpoint, object payload = null)
|
public async Task<T> GetGmxServiceAsync<T>(string endpoint, object? payload = null)
|
||||||
{
|
{
|
||||||
if (!endpoint.StartsWith("/"))
|
if (!endpoint.StartsWith("/"))
|
||||||
{
|
{
|
||||||
@@ -132,22 +200,9 @@ namespace Managing.Infrastructure.Evm.Services
|
|||||||
url += BuildQueryString(payload);
|
url += BuildQueryString(payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
return await ExecuteWithRetryAsync<T>(
|
||||||
{
|
() => _httpClient.GetAsync(url),
|
||||||
var response = await _httpClient.GetAsync(url);
|
$"GetGmxServiceAsync({endpoint})");
|
||||||
|
|
||||||
if (!response.IsSuccessStatusCode)
|
|
||||||
{
|
|
||||||
await HandleErrorResponse(response);
|
|
||||||
}
|
|
||||||
|
|
||||||
return await response.Content.ReadFromJsonAsync<T>(_jsonOptions);
|
|
||||||
}
|
|
||||||
catch (Exception ex) when (!(ex is Web3ProxyException))
|
|
||||||
{
|
|
||||||
SentrySdk.CaptureException(ex);
|
|
||||||
throw new Web3ProxyException($"Failed to get GMX service at {endpoint}: {ex.Message}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<GmxClaimableSummary> GetGmxClaimableSummaryAsync(string account)
|
public async Task<GmxClaimableSummary> GetGmxClaimableSummaryAsync(string account)
|
||||||
|
|||||||
58
src/Managing.Web3Proxy/README-REDIS.md
Normal file
58
src/Managing.Web3Proxy/README-REDIS.md
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
# Web3Proxy Redis Configuration
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
The Web3Proxy service now uses Redis for distributed idempotency storage across multiple instances.
|
||||||
|
|
||||||
|
### Required Environment Variables
|
||||||
|
|
||||||
|
- `REDIS_URL`: Redis connection string (default: `redis://localhost:6379`)
|
||||||
|
- `REDIS_PASSWORD`: Redis password (optional, for authenticated Redis instances)
|
||||||
|
- `LOG_LEVEL`: Logging level (default: `info`)
|
||||||
|
|
||||||
|
### Docker Configuration
|
||||||
|
|
||||||
|
When running in Docker, set the Redis URL to:
|
||||||
|
```
|
||||||
|
REDIS_URL=redis://redis:6379
|
||||||
|
```
|
||||||
|
|
||||||
|
For password-protected Redis instances:
|
||||||
|
```
|
||||||
|
REDIS_URL=redis://redis:6379
|
||||||
|
REDIS_PASSWORD=your_redis_password
|
||||||
|
```
|
||||||
|
|
||||||
|
### Production Configuration
|
||||||
|
|
||||||
|
For production deployments with password-protected Redis:
|
||||||
|
|
||||||
|
1. Set environment variables:
|
||||||
|
```bash
|
||||||
|
export REDIS_URL=redis://your-redis-host:6379
|
||||||
|
export REDIS_PASSWORD=your_secure_password
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Or use a connection string with embedded password:
|
||||||
|
```
|
||||||
|
REDIS_URL=redis://:your_password@your-redis-host:6379
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fallback Behavior
|
||||||
|
|
||||||
|
If Redis is not available, the service will automatically fall back to in-memory storage with a warning message. This ensures the service continues to work even without Redis, but idempotency will only work within a single instance.
|
||||||
|
|
||||||
|
### Production Deployment
|
||||||
|
|
||||||
|
For production deployments with multiple Web3Proxy instances:
|
||||||
|
|
||||||
|
1. Ensure Redis is running and accessible
|
||||||
|
2. Set the `REDIS_URL` environment variable
|
||||||
|
3. Monitor Redis connection status in logs
|
||||||
|
4. Consider Redis clustering for high availability
|
||||||
|
|
||||||
|
### Idempotency Key Format
|
||||||
|
|
||||||
|
Idempotency keys are stored in Redis with the prefix `idempotency:` and have a TTL of 5 minutes.
|
||||||
|
|
||||||
|
Example Redis key: `idempotency:123e4567-e89b-12d3-a456-426614174000`
|
||||||
97
src/Managing.Web3Proxy/package-lock.json
generated
97
src/Managing.Web3Proxy/package-lock.json
generated
@@ -40,12 +40,14 @@
|
|||||||
"mysql2": "^3.11.3",
|
"mysql2": "^3.11.3",
|
||||||
"postgrator": "^8.0.0",
|
"postgrator": "^8.0.0",
|
||||||
"query-string": "^9.1.1",
|
"query-string": "^9.1.1",
|
||||||
|
"redis": "^5.8.2",
|
||||||
"viem": "2.37.1",
|
"viem": "2.37.1",
|
||||||
"vitest": "^3.0.8",
|
"vitest": "^3.0.8",
|
||||||
"zod": "^3.24.2"
|
"zod": "^3.24.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^22.5.5",
|
"@types/node": "^22.5.5",
|
||||||
|
"@types/redis": "^4.0.10",
|
||||||
"c8": "^10.1.3",
|
"c8": "^10.1.3",
|
||||||
"eslint": "^9.11.0",
|
"eslint": "^9.11.0",
|
||||||
"fastify-tsconfig": "^3.0.0",
|
"fastify-tsconfig": "^3.0.0",
|
||||||
@@ -1521,6 +1523,66 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@redis/bloom": {
|
||||||
|
"version": "5.8.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-5.8.2.tgz",
|
||||||
|
"integrity": "sha512-855DR0ChetZLarblio5eM0yLwxA9Dqq50t8StXKp5bAtLT0G+rZ+eRzzqxl37sPqQKjUudSYypz55o6nNhbz0A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 18"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@redis/client": "^5.8.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@redis/client": {
|
||||||
|
"version": "5.8.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@redis/client/-/client-5.8.2.tgz",
|
||||||
|
"integrity": "sha512-WtMScno3+eBpTac1Uav2zugXEoXqaU23YznwvFgkPwBQVwEHTDgOG7uEAObtZ/Nyn8SmAMbqkEubJaMOvnqdsQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"cluster-key-slot": "1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@redis/json": {
|
||||||
|
"version": "5.8.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@redis/json/-/json-5.8.2.tgz",
|
||||||
|
"integrity": "sha512-uxpVfas3I0LccBX9rIfDgJ0dBrUa3+0Gc8sEwmQQH0vHi7C1Rx1Qn8Nv1QWz5bohoeIXMICFZRcyDONvum2l/w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 18"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@redis/client": "^5.8.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@redis/search": {
|
||||||
|
"version": "5.8.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@redis/search/-/search-5.8.2.tgz",
|
||||||
|
"integrity": "sha512-cNv7HlgayavCBXqPXgaS97DRPVWFznuzsAmmuemi2TMCx5scwLiP50TeZvUS06h/MG96YNPe6A0Zt57yayfxwA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 18"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@redis/client": "^5.8.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@redis/time-series": {
|
||||||
|
"version": "5.8.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-5.8.2.tgz",
|
||||||
|
"integrity": "sha512-g2NlHM07fK8H4k+613NBsk3y70R2JIM2dPMSkhIjl2Z17SYvaYKdusz85d7VYOrZBWtDrHV/WD2E3vGu+zni8A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 18"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@redis/client": "^5.8.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@rollup/rollup-darwin-arm64": {
|
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||||
"version": "4.35.0",
|
"version": "4.35.0",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
@@ -1769,6 +1831,16 @@
|
|||||||
"@types/pg": "*"
|
"@types/pg": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/redis": {
|
||||||
|
"version": "4.0.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/redis/-/redis-4.0.10.tgz",
|
||||||
|
"integrity": "sha512-7CLy5b5fzzEGVcOccgZjoMlNpPhX6d10jEeRy2YWbFuaMNrSPc9ExRsMYsd+0VxvEHucf4EWx24Ja7cSU1FGUA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"redis": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/shimmer": {
|
"node_modules/@types/shimmer": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/shimmer/-/shimmer-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/shimmer/-/shimmer-1.2.0.tgz",
|
||||||
@@ -2728,6 +2800,15 @@
|
|||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/cluster-key-slot": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/color-convert": {
|
"node_modules/color-convert": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -6550,6 +6631,22 @@
|
|||||||
"version": "0.5.1",
|
"version": "0.5.1",
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
|
"node_modules/redis": {
|
||||||
|
"version": "5.8.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/redis/-/redis-5.8.2.tgz",
|
||||||
|
"integrity": "sha512-31vunZj07++Y1vcFGcnNWEf5jPoTkGARgfWI4+Tk55vdwHxhAvug8VEtW7Cx+/h47NuJTEg/JL77zAwC6E0OeA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@redis/bloom": "5.8.2",
|
||||||
|
"@redis/client": "5.8.2",
|
||||||
|
"@redis/json": "5.8.2",
|
||||||
|
"@redis/search": "5.8.2",
|
||||||
|
"@redis/time-series": "5.8.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/reflect.getprototypeof": {
|
"node_modules/reflect.getprototypeof": {
|
||||||
"version": "1.0.10",
|
"version": "1.0.10",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
|||||||
@@ -59,12 +59,14 @@
|
|||||||
"mysql2": "^3.11.3",
|
"mysql2": "^3.11.3",
|
||||||
"postgrator": "^8.0.0",
|
"postgrator": "^8.0.0",
|
||||||
"query-string": "^9.1.1",
|
"query-string": "^9.1.1",
|
||||||
|
"redis": "^5.8.2",
|
||||||
"viem": "2.37.1",
|
"viem": "2.37.1",
|
||||||
"vitest": "^3.0.8",
|
"vitest": "^3.0.8",
|
||||||
"zod": "^3.24.2"
|
"zod": "^3.24.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^22.5.5",
|
"@types/node": "^22.5.5",
|
||||||
|
"@types/redis": "^4.0.10",
|
||||||
"c8": "^10.1.3",
|
"c8": "^10.1.3",
|
||||||
"eslint": "^9.11.0",
|
"eslint": "^9.11.0",
|
||||||
"fastify-tsconfig": "^3.0.0",
|
"fastify-tsconfig": "^3.0.0",
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import {FastifyPluginAsyncTypebox, Type} from '@fastify/type-provider-typebox'
|
|||||||
import {handleError} from '../utils/errorHandler.js'
|
import {handleError} from '../utils/errorHandler.js'
|
||||||
import {getClientForAddress} from '../plugins/custom/gmx.js'
|
import {getClientForAddress} from '../plugins/custom/gmx.js'
|
||||||
import {getPrivyClient} from '../plugins/custom/privy.js'
|
import {getPrivyClient} from '../plugins/custom/privy.js'
|
||||||
|
import {createClient} from 'redis'
|
||||||
|
|
||||||
const plugin: FastifyPluginAsyncTypebox = async (fastify) => {
|
const plugin: FastifyPluginAsyncTypebox = async (fastify) => {
|
||||||
fastify.get(
|
fastify.get(
|
||||||
@@ -48,6 +49,11 @@ const plugin: FastifyPluginAsyncTypebox = async (fastify) => {
|
|||||||
status: Type.String(),
|
status: Type.String(),
|
||||||
message: Type.String(),
|
message: Type.String(),
|
||||||
data: Type.Optional(Type.Any())
|
data: Type.Optional(Type.Any())
|
||||||
|
}),
|
||||||
|
redis: Type.Object({
|
||||||
|
status: Type.String(),
|
||||||
|
message: Type.String(),
|
||||||
|
data: Type.Optional(Type.Any())
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
@@ -59,9 +65,11 @@ const plugin: FastifyPluginAsyncTypebox = async (fastify) => {
|
|||||||
}
|
}
|
||||||
}, async function (request, reply) {
|
}, async function (request, reply) {
|
||||||
try {
|
try {
|
||||||
|
console.log('Checking health...')
|
||||||
const checks = {
|
const checks = {
|
||||||
privy: await checkPrivy(fastify),
|
privy: await checkPrivy(fastify),
|
||||||
gmx: await checkGmx()
|
gmx: await checkGmx(),
|
||||||
|
redis: await checkRedis()
|
||||||
}
|
}
|
||||||
|
|
||||||
// If any check failed, set status to degraded
|
// If any check failed, set status to degraded
|
||||||
@@ -150,6 +158,96 @@ const plugin: FastifyPluginAsyncTypebox = async (fastify) => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper function to check Redis connectivity for idempotency
|
||||||
|
async function checkRedis() {
|
||||||
|
let redisClient = null;
|
||||||
|
try {
|
||||||
|
const redisUrl = process.env.REDIS_URL || 'redis://localhost:6379';
|
||||||
|
const redisPassword = process.env.REDIS_PASSWORD;
|
||||||
|
|
||||||
|
console.log('Redis URL:', redisUrl)
|
||||||
|
console.log('Redis Password:', redisPassword)
|
||||||
|
|
||||||
|
// Create Redis client configuration
|
||||||
|
const redisConfig: any = { url: redisUrl };
|
||||||
|
// if (redisPassword) {
|
||||||
|
// redisConfig.password = redisPassword;
|
||||||
|
// }
|
||||||
|
|
||||||
|
redisClient = createClient(redisConfig);
|
||||||
|
|
||||||
|
// Set up error handling
|
||||||
|
redisClient.on('error', (err) => {
|
||||||
|
console.error('Redis health check error:', err);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Connect to Redis
|
||||||
|
const startTime = Date.now();
|
||||||
|
await redisClient.connect();
|
||||||
|
const connectTime = Date.now() - startTime;
|
||||||
|
|
||||||
|
// Test basic operations
|
||||||
|
const testKey = 'health-check-test';
|
||||||
|
const testValue = JSON.stringify({ timestamp: Date.now(), test: true });
|
||||||
|
|
||||||
|
// Test SET operation
|
||||||
|
await redisClient.set(testKey, testValue, { EX: 10 }); // 10 second expiry
|
||||||
|
|
||||||
|
// Test GET operation
|
||||||
|
const retrievedValue = await redisClient.get(testKey);
|
||||||
|
const getTime = Date.now() - startTime;
|
||||||
|
|
||||||
|
// Test JSON parsing
|
||||||
|
const parsedValue = JSON.parse(retrievedValue as string);
|
||||||
|
|
||||||
|
// Clean up test key
|
||||||
|
await redisClient.del(testKey);
|
||||||
|
|
||||||
|
// Get Redis info
|
||||||
|
const info = await redisClient.info('server');
|
||||||
|
const serverInfo = info.split('\r\n').reduce((acc, line) => {
|
||||||
|
const [key, value] = line.split(':');
|
||||||
|
if (key && value) {
|
||||||
|
acc[key] = value;
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<string, string>);
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: 'healthy',
|
||||||
|
message: 'Redis connection successful',
|
||||||
|
data: {
|
||||||
|
connectTimeMs: connectTime,
|
||||||
|
getTimeMs: getTime,
|
||||||
|
redisVersion: serverInfo.redis_version,
|
||||||
|
uptimeSeconds: serverInfo.uptime_in_seconds,
|
||||||
|
connectedClients: serverInfo.connected_clients,
|
||||||
|
usedMemory: serverInfo.used_memory_human,
|
||||||
|
hasPassword: !!redisPassword
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
status: 'unhealthy',
|
||||||
|
message: `Redis connection failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||||
|
data: {
|
||||||
|
errorType: error instanceof Error ? error.constructor.name : 'Unknown',
|
||||||
|
redisUrl: process.env.REDIS_URL || 'redis://localhost:6379',
|
||||||
|
hasPassword: !!process.env.REDIS_PASSWORD
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} finally {
|
||||||
|
// Always close the Redis connection
|
||||||
|
if (redisClient && redisClient.isOpen) {
|
||||||
|
try {
|
||||||
|
await redisClient.quit();
|
||||||
|
} catch (closeError) {
|
||||||
|
console.error('Error closing Redis connection in health check:', closeError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default plugin
|
export default plugin
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
import Fastify from 'fastify'
|
import Fastify from 'fastify'
|
||||||
import fp from 'fastify-plugin'
|
import fp from 'fastify-plugin'
|
||||||
|
import {createClient, RedisClientType} from 'redis'
|
||||||
|
|
||||||
// Import library to exit fastify process, gracefully (if possible)
|
// Import library to exit fastify process, gracefully (if possible)
|
||||||
import closeWithGrace from 'close-with-grace'
|
import closeWithGrace from 'close-with-grace'
|
||||||
@@ -14,6 +15,94 @@ import closeWithGrace from 'close-with-grace'
|
|||||||
// Import your application as a normal plugin.
|
// Import your application as a normal plugin.
|
||||||
import serviceApp from './app.js'
|
import serviceApp from './app.js'
|
||||||
|
|
||||||
|
// Idempotency storage using Redis
|
||||||
|
interface IdempotencyEntry {
|
||||||
|
requestId: string
|
||||||
|
response: any
|
||||||
|
statusCode: number
|
||||||
|
timestamp: number
|
||||||
|
ttl: number // Time to live in milliseconds
|
||||||
|
}
|
||||||
|
|
||||||
|
let redisClient: RedisClientType | null = null
|
||||||
|
const IDEMPOTENCY_TTL = 5 * 60 // 5 minutes TTL in seconds (Redis uses seconds)
|
||||||
|
|
||||||
|
// Initialize Redis connection
|
||||||
|
async function initializeRedis() {
|
||||||
|
try {
|
||||||
|
const redisUrl = process.env.REDIS_URL || 'redis://localhost:6379'
|
||||||
|
const redisPassword = process.env.REDIS_PASSWORD
|
||||||
|
|
||||||
|
console.log('Redis URL:', redisUrl)
|
||||||
|
console.log('Redis Password:', redisPassword)
|
||||||
|
|
||||||
|
// Create Redis client with password support
|
||||||
|
const redisConfig: any = { url: redisUrl }
|
||||||
|
|
||||||
|
if (redisPassword) {
|
||||||
|
redisConfig.password = redisPassword
|
||||||
|
}
|
||||||
|
|
||||||
|
redisClient = createClient(redisConfig)
|
||||||
|
|
||||||
|
redisClient.on('error', (err) => {
|
||||||
|
console.error('Redis Client Error:', err)
|
||||||
|
})
|
||||||
|
|
||||||
|
redisClient.on('connect', () => {
|
||||||
|
console.log('Connected to Redis for idempotency')
|
||||||
|
})
|
||||||
|
|
||||||
|
redisClient.on('ready', () => {
|
||||||
|
console.log('Redis client ready for idempotency operations')
|
||||||
|
})
|
||||||
|
|
||||||
|
await redisClient.connect()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to connect to Redis:', error)
|
||||||
|
// Fallback to in-memory storage if Redis is not available
|
||||||
|
console.warn('Falling back to in-memory idempotency storage')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback in-memory storage for when Redis is not available
|
||||||
|
const fallbackStore = new Map<string, IdempotencyEntry>()
|
||||||
|
|
||||||
|
// Helper function to get idempotency entry
|
||||||
|
async function getIdempotencyEntry(requestId: string): Promise<IdempotencyEntry | null> {
|
||||||
|
if (redisClient && redisClient.isOpen) {
|
||||||
|
try {
|
||||||
|
const data = await redisClient.get(`idempotency:${requestId}`)
|
||||||
|
if (data && typeof data === 'string') {
|
||||||
|
return JSON.parse(data)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Redis get error:', error)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fallback to in-memory storage
|
||||||
|
return fallbackStore.get(requestId) || null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to set idempotency entry
|
||||||
|
async function setIdempotencyEntry(requestId: string, entry: IdempotencyEntry): Promise<void> {
|
||||||
|
if (redisClient && redisClient.isOpen) {
|
||||||
|
try {
|
||||||
|
await redisClient.setEx(`idempotency:${requestId}`, IDEMPOTENCY_TTL, JSON.stringify(entry))
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Redis set error:', error)
|
||||||
|
// Fallback to in-memory storage
|
||||||
|
fallbackStore.set(requestId, entry)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fallback to in-memory storage
|
||||||
|
fallbackStore.set(requestId, entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Do not use NODE_ENV to determine what logger (or any env related feature) to use
|
* Do not use NODE_ENV to determine what logger (or any env related feature) to use
|
||||||
* @see {@link https://www.youtube.com/watch?v=HMM7GJC5E2o}
|
* @see {@link https://www.youtube.com/watch?v=HMM7GJC5E2o}
|
||||||
@@ -33,7 +122,7 @@ function getLoggerOptions () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { level: process.env.LOG_LEVEL ?? 'silent' }
|
return { level: process.env.LOG_LEVEL ?? 'info' } // Changed from 'silent' to 'info' for better debugging
|
||||||
}
|
}
|
||||||
|
|
||||||
const app = Fastify({
|
const app = Fastify({
|
||||||
@@ -43,22 +132,108 @@ const app = Fastify({
|
|||||||
coerceTypes: 'array', // change type of data to match type keyword
|
coerceTypes: 'array', // change type of data to match type keyword
|
||||||
removeAdditional: 'all' // Remove additional body properties
|
removeAdditional: 'all' // Remove additional body properties
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
// Add connection and timeout settings for better resilience
|
||||||
|
connectionTimeout: 30000, // 30 seconds
|
||||||
|
keepAliveTimeout: 5000, // 5 seconds
|
||||||
|
bodyLimit: 1048576, // 1MB
|
||||||
|
maxParamLength: 200, // 200 characters
|
||||||
|
// Add request timeout
|
||||||
|
requestTimeout: 30000, // 30 seconds
|
||||||
})
|
})
|
||||||
|
|
||||||
async function init () {
|
async function init () {
|
||||||
|
// Initialize Redis connection
|
||||||
|
await initializeRedis()
|
||||||
|
|
||||||
|
// Add idempotency pre-handler hook
|
||||||
|
app.addHook('preHandler', async (request, reply) => {
|
||||||
|
// Only apply idempotency to POST requests (trading operations)
|
||||||
|
if (request.method !== 'POST') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestId = request.headers['idempotency-key'] || request.headers['x-request-id']
|
||||||
|
|
||||||
|
if (!requestId) {
|
||||||
|
// No idempotency key provided, continue normally
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we've seen this request before
|
||||||
|
const existingEntry = await getIdempotencyEntry(requestId as string)
|
||||||
|
|
||||||
|
if (existingEntry) {
|
||||||
|
// Check if entry is still valid (Redis TTL handles this, but double-check for fallback)
|
||||||
|
const now = Date.now()
|
||||||
|
if (now - existingEntry.timestamp <= existingEntry.ttl) {
|
||||||
|
app.log.info(`Idempotency: Returning cached response for request ${requestId}`)
|
||||||
|
|
||||||
|
// Return the cached response
|
||||||
|
reply.code(existingEntry.statusCode)
|
||||||
|
return existingEntry.response
|
||||||
|
} else {
|
||||||
|
// Entry expired, remove it (only needed for fallback storage)
|
||||||
|
if (!redisClient || !redisClient.isOpen) {
|
||||||
|
fallbackStore.delete(requestId as string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the request ID for later use
|
||||||
|
request.idempotencyKey = requestId as string
|
||||||
|
})
|
||||||
|
|
||||||
|
// Add post-handler hook to store successful responses
|
||||||
|
app.addHook('onSend', async (request, reply, payload) => {
|
||||||
|
if (request.idempotencyKey && request.method === 'POST') {
|
||||||
|
const requestId = request.idempotencyKey
|
||||||
|
|
||||||
|
// Only store successful responses (2xx status codes)
|
||||||
|
if (reply.statusCode >= 200 && reply.statusCode < 300) {
|
||||||
|
try {
|
||||||
|
const responseData = typeof payload === 'string' ? JSON.parse(payload) : payload
|
||||||
|
|
||||||
|
const entry: IdempotencyEntry = {
|
||||||
|
requestId,
|
||||||
|
response: responseData,
|
||||||
|
statusCode: reply.statusCode,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
ttl: IDEMPOTENCY_TTL * 1000 // Convert to milliseconds for consistency
|
||||||
|
}
|
||||||
|
|
||||||
|
await setIdempotencyEntry(requestId, entry)
|
||||||
|
|
||||||
|
app.log.info(`Idempotency: Stored response for request ${requestId}`)
|
||||||
|
} catch (error) {
|
||||||
|
app.log.error(`Idempotency: Failed to store response for request ${requestId}:`, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// Register your application as a normal plugin.
|
// Register your application as a normal plugin.
|
||||||
// fp must be used to override default error handler
|
// fp must be used to override default error handler
|
||||||
app.register(fp(serviceApp))
|
app.register(fp(serviceApp))
|
||||||
|
|
||||||
// Delay is the number of milliseconds for the graceful close to finish
|
// Delay is the number of milliseconds for the graceful close to finish
|
||||||
closeWithGrace(
|
closeWithGrace(
|
||||||
{ delay: process.env.FASTIFY_CLOSE_GRACE_DELAY ?? 500 },
|
{ delay: process.env.FASTIFY_CLOSE_GRACE_DELAY ?? 5000 }, // Increased from 500ms to 5s
|
||||||
async ({ err }) => {
|
async ({ err }) => {
|
||||||
if (err != null) {
|
if (err != null) {
|
||||||
app.log.error(err)
|
app.log.error(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Close Redis connection gracefully
|
||||||
|
if (redisClient && redisClient.isOpen) {
|
||||||
|
try {
|
||||||
|
await redisClient.quit()
|
||||||
|
console.log('Redis connection closed gracefully')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error closing Redis connection:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await app.close()
|
await app.close()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -66,8 +241,14 @@ async function init () {
|
|||||||
await app.ready()
|
await app.ready()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Start listening.
|
// Start listening with better configuration
|
||||||
await app.listen({ port: 4111 })
|
await app.listen({
|
||||||
|
port: 4111,
|
||||||
|
host: '0.0.0.0', // Listen on all interfaces
|
||||||
|
backlog: 511, // Increase backlog for better connection handling
|
||||||
|
})
|
||||||
|
|
||||||
|
app.log.info('Web3Proxy server started successfully on port 4111')
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
app.log.error(err)
|
app.log.error(err)
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
|
|||||||
5
src/Managing.Web3Proxy/src/types/fastify.d.ts
vendored
Normal file
5
src/Managing.Web3Proxy/src/types/fastify.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
declare module 'fastify' {
|
||||||
|
interface FastifyRequest {
|
||||||
|
idempotencyKey?: string
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,6 +18,29 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- managing-network
|
- managing-network
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:7-alpine
|
||||||
|
ports:
|
||||||
|
- "6379:6379"
|
||||||
|
volumes:
|
||||||
|
- redis_data:/data
|
||||||
|
networks:
|
||||||
|
- managing-network
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- REDIS_PASSWORD=${REDIS_PASSWORD:-}
|
||||||
|
command: >
|
||||||
|
sh -c "
|
||||||
|
if [ -n \"$$REDIS_PASSWORD\" ]; then
|
||||||
|
redis-server --appendonly yes --requirepass $$REDIS_PASSWORD
|
||||||
|
else
|
||||||
|
redis-server --appendonly yes
|
||||||
|
fi
|
||||||
|
"
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
redis_data:
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
managing-network:
|
managing-network:
|
||||||
external:
|
external:
|
||||||
|
|||||||
Reference in New Issue
Block a user