Skip to content

Commit 20ee9dc

Browse files
committed
Run smoke tests for connectivity purposes
1 parent 0af5208 commit 20ee9dc

File tree

10 files changed

+184
-66
lines changed

10 files changed

+184
-66
lines changed

.github/workflows/cd.yml

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,4 +57,16 @@ jobs:
5757
--cache-from=type=local,src=/tmp/.buildx-cache \
5858
--cache-to=type=local,dest=/tmp/.buildx-cache \
5959
--load \
60-
./webapp
60+
./webapp
61+
62+
- name: Start docker-compose
63+
run: |
64+
docker-compose -f docker-compose.yml up -d --build
65+
66+
- smoke-test:
67+
image: curlimages/curl
68+
depends_on:
69+
- webapp
70+
- api
71+
entrypoint: >
72+
sh -c "sleep 5 && curl -sf http://localhost:3001 || exit 1"

api/src/infrastructure/infrastructure.module.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ import { Module, DynamicModule } from '@nestjs/common';
22
import { ConfigModule } from '@nestjs/config';
33
import { ShortenUrlRepository } from '../shorten-url/shorten-url.repository';
44
import { ShortenUrlRepositoryFactory } from './repository/shorten-url/shorten-url-repository.factory';
5-
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
6-
import { DynamoClientProvider } from './provider/dynamodb-client.provider';
5+
import { DynamoDbClientProvider } from './provider/dynamo-db-client-provider.service';
76
import { RedisClientProvider } from './provider/redis-client.provider';
7+
import { HealthController } from './monitoring/health.controller';
88

99
@Module({
1010
imports: [ConfigModule],
@@ -13,12 +13,17 @@ export class InfrastructureModule {
1313
static register(): DynamicModule {
1414
return {
1515
module: InfrastructureModule,
16+
controllers: [HealthController],
1617
providers: [
1718
RedisClientProvider,
18-
DynamoClientProvider,
19+
DynamoDbClientProvider,
1920
ShortenUrlRepositoryFactory,
2021
],
21-
exports: [RedisClientProvider, DynamoDBClient, ShortenUrlRepository],
22+
exports: [
23+
RedisClientProvider,
24+
DynamoDbClientProvider,
25+
ShortenUrlRepository,
26+
],
2227
};
2328
}
2429
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { Controller, Get, Inject } from '@nestjs/common';
2+
import { ConfigService } from '@nestjs/config';
3+
import { RedisClientProvider } from '../provider/redis-client.provider';
4+
import { DynamoDbClientProvider } from '../provider/dynamo-db-client-provider';
5+
6+
@Controller('monitoring')
7+
export class HealthController {
8+
constructor(
9+
private readonly configService: ConfigService,
10+
private readonly redisClient: RedisClientProvider | null,
11+
private readonly dynamoDbClient: DynamoDbClientProvider | null,
12+
) {}
13+
14+
@Get('/readiness')
15+
async readiness(): Promise<{ status: string }> {
16+
const usePersistenceStorage = this.configService.get<string>(
17+
'USE_PERSISTENT_STORAGE',
18+
'true',
19+
);
20+
21+
if (usePersistenceStorage === 'false') {
22+
return { status: 'OK' };
23+
}
24+
25+
return (await this.dynamoDbClient.isReady()) &&
26+
(await this.redisClient.isReady())
27+
? { status: 'OK' }
28+
: { status: 'KO' };
29+
}
30+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import {
2+
Injectable,
3+
Logger,
4+
OnModuleDestroy,
5+
OnModuleInit,
6+
} from '@nestjs/common';
7+
import { ConfigService } from '@nestjs/config';
8+
import {
9+
DynamoDBClient,
10+
GetItemCommand,
11+
PutItemCommand,
12+
ListTablesCommand,
13+
} from '@aws-sdk/client-dynamodb';
14+
import { fromEnv } from '@aws-sdk/credential-providers';
15+
import { unmarshall } from '@aws-sdk/util-dynamodb';
16+
17+
@Injectable()
18+
export class DynamoDbClientProvider implements OnModuleInit, OnModuleDestroy {
19+
private dynamoDBClient: DynamoDBClient | null = null;
20+
21+
constructor(private readonly configService: ConfigService) {}
22+
23+
async putItem(key: string, value: string, tableName: string): Promise<void> {
24+
if (!this.dynamoDBClient)
25+
throw new Error('DynamoDB client not initialized');
26+
27+
await this.dynamoDBClient.send(
28+
new PutItemCommand({
29+
TableName: tableName,
30+
Item: {
31+
shortId: { S: key },
32+
originalUrl: { S: value },
33+
createdAt: { S: new Date().toISOString() },
34+
},
35+
}),
36+
);
37+
}
38+
39+
async getItem(
40+
key: string,
41+
tableName: string,
42+
): Promise<Record<string, any> | null> {
43+
if (!this.dynamoDBClient)
44+
throw new Error('DynamoDB client not initialized');
45+
46+
const result = await this.dynamoDBClient.send(
47+
new GetItemCommand({
48+
TableName: tableName,
49+
Key: { shortId: { S: key } },
50+
}),
51+
);
52+
53+
return unmarshall(result.Item);
54+
}
55+
56+
async isReady(): Promise<boolean> {
57+
if (!this.dynamoDBClient) return false;
58+
try {
59+
await this.dynamoDBClient.send(new ListTablesCommand({}));
60+
return true;
61+
} catch (err) {
62+
Logger.error('DynamoDB readiness check failed', err);
63+
return false;
64+
}
65+
}
66+
67+
async onModuleInit() {
68+
const usePersistent = this.configService.get<string>(
69+
'USE_PERSISTENT_STORAGE',
70+
'false',
71+
);
72+
73+
if (usePersistent === 'false') {
74+
Logger.log('🧪 Skipping dynamo connection (non-persistent mode)');
75+
return;
76+
}
77+
78+
this.dynamoDBClient = new DynamoDBClient({
79+
region: this.configService.get('AWS_REGION', 'local'),
80+
endpoint: this.configService.get(
81+
'DYNAMO_ENDPOINT',
82+
'http://localhost:8000',
83+
),
84+
credentials: fromEnv({}),
85+
});
86+
Logger.log('Connected to dynamodb');
87+
}
88+
89+
async onModuleDestroy() {
90+
if (this.dynamoDBClient) {
91+
this.dynamoDBClient.destroy();
92+
Logger.log('Dynamodb connection closed');
93+
}
94+
}
95+
}

api/src/infrastructure/provider/dynamodb-client.provider.ts

Lines changed: 0 additions & 27 deletions
This file was deleted.

api/src/infrastructure/provider/redis-client.provider.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,16 @@ export class RedisClientProvider implements OnModuleInit, OnModuleDestroy {
2121
await this.client.set(key, value, { EX: ttlInSeconds });
2222
}
2323

24+
async isReady(): Promise<boolean> {
25+
try {
26+
await this.client.ping();
27+
return true;
28+
} catch (error) {
29+
Logger.error('Redis client is not ready', error);
30+
return false;
31+
}
32+
}
33+
2434
async onModuleInit() {
2535
const usePersistent = this.configService.get<string>(
2636
'USE_PERSISTENT_STORAGE',
@@ -33,7 +43,19 @@ export class RedisClientProvider implements OnModuleInit, OnModuleDestroy {
3343
}
3444

3545
const redisUrl = this.configService.get<string>('REDIS_URL');
36-
this.client = createClient({ url: redisUrl });
46+
this.client = createClient({
47+
url: redisUrl,
48+
socket: {
49+
reconnectStrategy: (retries) => {
50+
if (retries > 5) {
51+
Logger.warn('🔌 Redis reconnect limit reached');
52+
return new Error('Stop retrying Redis');
53+
}
54+
Logger.warn('Redis reconnecting...', retries);
55+
return 30_000;
56+
},
57+
},
58+
});
3759

3860
this.client.on('error', (err) =>
3961
Logger.error('Redis client error', err, 'REDIS_CLIENT'),

api/src/infrastructure/repository/shorten-url/persistent-url-storage.repository.ts

Lines changed: 5 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,9 @@
11
import { ShortenUrlRepository } from '../../../shorten-url/shorten-url.repository';
2-
import { Inject, Injectable, Logger } from '@nestjs/common';
2+
import { Injectable, Logger } from '@nestjs/common';
33
import { ConfigService } from '@nestjs/config';
4-
import {
5-
DynamoDBClient,
6-
PutItemCommand,
7-
GetItemCommand,
8-
} from '@aws-sdk/client-dynamodb';
94
import { unmarshall } from '@aws-sdk/util-dynamodb';
105
import { RedisClientProvider } from '../../provider/redis-client.provider';
6+
import { DynamoDbClientProvider } from '../../provider/dynamo-db-client-provider.service';
117

128
@Injectable()
139
export class PersistentUrlStorageRepository implements ShortenUrlRepository {
@@ -18,8 +14,7 @@ export class PersistentUrlStorageRepository implements ShortenUrlRepository {
1814
constructor(
1915
private readonly configService: ConfigService,
2016
private readonly redisClient: RedisClientProvider | null,
21-
@Inject(DynamoDBClient)
22-
private readonly dynamoDbClient: DynamoDBClient | null,
17+
private readonly dynamoDbClient: DynamoDbClientProvider | null,
2318
) {
2419
this.redisTTL = this.configService.get<number>(
2520
'REDIS_TTL',
@@ -32,17 +27,7 @@ export class PersistentUrlStorageRepository implements ShortenUrlRepository {
3227
const now = new Date().toISOString();
3328

3429
// Write to DynamoDB
35-
await this.dynamoDbClient.send(
36-
new PutItemCommand({
37-
TableName: this.tableName,
38-
Item: {
39-
shortId: { S: shortId },
40-
originalUrl: { S: originalUrl },
41-
createdAt: { S: now },
42-
},
43-
}),
44-
);
45-
30+
await this.dynamoDbClient.putItem(shortId, originalUrl, this.tableName);
4631
// Cache in Redis
4732
await this.redisClient.set(`short:${shortId}`, originalUrl, this.redisTTL);
4833
}
@@ -58,12 +43,7 @@ export class PersistentUrlStorageRepository implements ShortenUrlRepository {
5843
Logger.debug(`Cache miss for ${shortId}. Fetching from DynamoDB...`);
5944

6045
// Fall back to DynamoDB
61-
const result = await this.dynamoDbClient.send(
62-
new GetItemCommand({
63-
TableName: this.tableName,
64-
Key: { shortId: { S: shortId } },
65-
}),
66-
);
46+
const result = await this.dynamoDbClient.getItem(shortId, this.tableName);
6747

6848
if (!result.Item) return null;
6949

api/src/infrastructure/repository/shorten-url/shorten-url-repository.factory.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { ShortenUrlRepository } from '../../../shorten-url/shorten-url.repositor
55
import { PersistentUrlStorageRepository } from './persistent-url-storage.repository';
66
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
77
import { RedisClientProvider } from '../../provider/redis-client.provider';
8+
import { DynamoDbClientProvider } from '../../provider/dynamo-db-client-provider.service';
89

910
/**
1011
* Factory provider to select the appropriate repository implementation based on configuration.
@@ -14,8 +15,8 @@ export const ShortenUrlRepositoryFactory: Provider = {
1415
provide: ShortenUrlRepository,
1516
useFactory: (
1617
configService: ConfigService,
17-
redisClientService: RedisClientProvider | null,
18-
dynamoDbClient: DynamoDBClient | null,
18+
redisClientProvider: RedisClientProvider | null,
19+
dynamoDbClientProvider: DynamoDbClientProvider | null,
1920
) => {
2021
const usePersistenceStorage = configService.get<string>(
2122
'USE_PERSISTENT_STORAGE',
@@ -25,12 +26,12 @@ export const ShortenUrlRepositoryFactory: Provider = {
2526
if (usePersistenceStorage === 'true') {
2627
return new PersistentUrlStorageRepository(
2728
configService,
28-
redisClientService,
29-
dynamoDbClient,
29+
redisClientProvider,
30+
dynamoDbClientProvider,
3031
);
3132
}
3233

3334
return new InMemoryUrlStorageRepository();
3435
},
35-
inject: [ConfigService, RedisClientProvider, DynamoDBClient],
36+
inject: [ConfigService, RedisClientProvider, DynamoDbClientProvider],
3637
};

api/src/main.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,9 @@ async function bootstrap() {
3636
);
3737

3838
const port = process.env.PORT || 3000;
39-
await app.listen(port).then(() => Logger.log(`Server started on ${port}`));
39+
await app
40+
.listen(port)
41+
.then(() => Logger.log(`API Server started on ${port}`));
4042
}
4143

4244
bootstrap();

docker-compose.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,6 @@ services:
7575
- ./scripts:/scripts
7676
entrypoint: ["/bin/sh", "-c", "/scripts/init-dynamodb.sh"]
7777

78-
79-
8078
volumes:
8179
redis_data:
8280
driver: local

0 commit comments

Comments
 (0)