Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ dev-web:

dev-worker-orchestrator:
@echo "Starting Orchestrator Worker for local development..."
ENVIRONMENT=development PYTHONPATH=services/workers/orchestrator/src:libs/database/src:libs/config/src:libs/pipeline/src:libs/domain/src:libs/transformer/src uv run python services/workers/orchestrator/src/worker/main.py
ENVIRONMENT=development PYTHONPATH=services/workers/orchestrator/src:libs/database/src:libs/config/src:libs/pipeline/src:libs/domain/src:libs/transformer/src:libs/scheduler/src uv run python services/workers/orchestrator/src/worker/main.py

dev-worker-compute:
@echo "Starting Compute Worker for local development..."
Expand Down Expand Up @@ -92,7 +92,7 @@ db-sqs-reset: db-reset sqs-purge

clear-data:
@echo "Clearing data plane tables (edi_message, edi_json, api_gateway, outbox) and purging SQS..."
uv run python scripts/clear_data.py
uv run python scripts/clear_data.py --i-am-sure

seed: db-init

Expand Down
29 changes: 23 additions & 6 deletions TECHNICAL_DEBT.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,17 @@ To achieve the long-term vision of completely merging and modernizing `bots_core
## 2. Outbox Sweeper (CDC Fallback Relay)

**Description:**
The system currently relies exclusively on Debezium (CDC) reading the PostgreSQL Write-Ahead Log (WAL) to route `Outbox` events to SQS. If Debezium crashes, loses offsets, or experiences network partitioning, `PENDING` outbox events will be permanently trapped in the database, breaking the asynchronous event pipeline.
The Outbox Sweeper has been implemented as a fallback to Debezium (CDC). It iterates over all shards to relay `PENDING` outbox events to SQS. However, the current implementation iterates over shards sequentially (`for shard in shards: await self._sweep_shard(...)`). As the number of shards grows in the multi-tenant architecture, this sequential sweep will take longer and potentially exceed the polling interval, causing lag.

**Proposed Resolution:**
Implement an Outbox Sweeper background worker that acts as a robust enterprise fallback and garbage collector:
1. **Fallback Poller:** A cron/scheduled task that periodically queries `SELECT * FROM outbox WHERE status = 'PENDING'` for events older than a configured threshold (e.g., 60 seconds) and manually relays them to SQS.
2. **Garbage Collector:** A cleanup task that runs `DELETE FROM outbox WHERE status = 'COMPLETED'` for events older than 7 days to prevent unbounded database growth.
Refactor the Outbox Sweeper to use Bounded Concurrency or Distributed Job Fan-out:
1. **Bounded Concurrency:** Run sweeps concurrently using `asyncio.gather` bounded by an `asyncio.Semaphore` so multiple shards are swept at once without exhausting resources.
2. **Distributed Job Fan-out:** Instead of a single job, spawn a `ScheduledJob` for each shard dynamically, allowing multiple orchestrator pods to load balance the shard sweeping.
3. **Garbage Collector (Pending):** A cleanup task that runs `DELETE FROM outbox WHERE status = 'COMPLETED'` for events older than 7 days to prevent unbounded database growth is still needed.

**Estimated Effort:** Low
**Estimated Effort:** Low-Medium
**Estimated Time:** 1 to 2 days
**Impact:** Essential for enterprise-grade high availability. Guarantees no messages are ever lost due to CDC infrastructure failures and keeps the database optimized over time.
**Impact:** Prevents the sweeper from falling behind as the number of database shards scales, ensuring enterprise-grade multi-tenant reliability.

## 3. AS2 Protocol

Expand Down Expand Up @@ -80,3 +81,19 @@ Currently, the bots engine does not support a lightweight validation mode (e.g.,
### UnitOfWork Architecture (Control Plane vs Data Plane Naming)
Currently, the `UnitOfWork` (and its underlying SQL Alchemy repositories) leak infrastructure/deployment boundaries ("Control Plane" and "Data Plane") into domain business logic. We have giant God-objects like `SqlAlchemyControlPlaneRepository` inheriting from 10+ distinct repositories, causing namespace collisions and violating SOLID principles (Single Responsibility Principle).
**Future Action:** Refactor `UnitOfWork` to remove `control_plane` and `data_plane` concepts from class names and properties. Use Composition to expose distinct Bounded Contexts (e.g., `self.trading_partners`, `self.transactions`, `self.routes`) instead of lumping them into control/data plane buckets.

## 8. Hybrid SQS Tenancy (Dynamic Queue Resolution)

**Description:**
Currently, both inbound and outbound events are routed to a static, shared SQS queue (e.g., `TransformOrchestrationQueue`). In a multi-tenant environment, a massive batch of outbound events from one tenant can block critical inbound processing for all other tenants (the "noisy neighbor" problem).

**Proposed Resolution:**
Implement a Hybrid SQS routing model that dynamically resolves the target queue based on the tenant's tier and the event direction:
1. **Dynamic Queue Resolver:** The CDC Relay and Outbox Sweeper should read the `tenant_id` from the outbox event and lookup the tenant's tier (cached in memory).
2. **Standard Tenants:** Route inbound events to `standard-inbound-queue` and outbound events to `standard-outbound-queue`.
3. **Enterprise Tenants:** Route events to dedicated queues (e.g., `enterprise-{tenant_name}-inbound-queue`).
4. **Dedicated Workers:** Deploy separate worker pods for standard inbound, standard outbound, and dedicated enterprise queues to provide strict compute isolation.

**Estimated Effort:** Medium
**Estimated Time:** 3 to 5 days
**Impact:** Essential for enterprise-grade SaaS scaling. Guarantees compute isolation for enterprise customers and bulkheads heavy outbound processing from blocking high-priority inbound traffic.
10 changes: 9 additions & 1 deletion docker/localstack/init-aws.sh
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@ awslocal sqs create-queue --queue-name CDC-DLQ

awslocal sqs create-queue --queue-name TransformOrchestrationQueue-DLQ
TRANSFORM_DLQ_ARN=$(awslocal sqs get-queue-attributes --queue-url http://localhost:4566/000000000000/TransformOrchestrationQueue-DLQ --attribute-names QueueArn --query 'Attributes.QueueArn' --output text)
awslocal sqs create-queue --queue-name TransformOrchestrationQueue --attributes "{\"RedrivePolicy\":\"{\\\"deadLetterTargetArn\\\":\\\"$TRANSFORM_DLQ_ARN\\\",\\\"maxReceiveCount\\\":\\\"3\\\"}\"}"
awslocal sqs create-queue --queue-name TransformOrchestrationQueue --attributes "{\"RedrivePolicy\":\"{\\\"deadLetterTargetArn\\\":\\\"$TRANSFORM_DLQ_ARN\\\",\\\"maxReceiveCount\\\":\\\"3\\\"}\"}"

awslocal sqs create-queue --queue-name TransformComputeQueue-DLQ
COMPUTE_DLQ_ARN=$(awslocal sqs get-queue-attributes --queue-url http://localhost:4566/000000000000/TransformComputeQueue-DLQ --attribute-names QueueArn --query 'Attributes.QueueArn' --output text)
awslocal sqs create-queue --queue-name TransformComputeQueue --attributes "{\"RedrivePolicy\":\"{\\\"deadLetterTargetArn\\\":\\\"$COMPUTE_DLQ_ARN\\\",\\\"maxReceiveCount\\\":\\\"3\\\"}\"}"

awslocal sqs create-queue --queue-name DeliverQueue-DLQ
DELIVER_DLQ_ARN=$(awslocal sqs get-queue-attributes --queue-url http://localhost:4566/000000000000/DeliverQueue-DLQ --attribute-names QueueArn --query 'Attributes.QueueArn' --output text)
Expand All @@ -25,4 +29,8 @@ awslocal sqs create-queue --queue-name ProvisioningQueue-DLQ
PROVISIONING_DLQ_ARN=$(awslocal sqs get-queue-attributes --queue-url http://localhost:4566/000000000000/ProvisioningQueue-DLQ --attribute-names QueueArn --query 'Attributes.QueueArn' --output text)
awslocal sqs create-queue --queue-name ProvisioningQueue --attributes "{\"RedrivePolicy\":\"{\\\"deadLetterTargetArn\\\":\\\"$PROVISIONING_DLQ_ARN\\\",\\\"maxReceiveCount\\\":\\\"3\\\"}\"}"

awslocal sqs create-queue --queue-name edi-orchestrator-jobs-DLQ
JOBS_DLQ_ARN=$(awslocal sqs get-queue-attributes --queue-url http://localhost:4566/000000000000/edi-orchestrator-jobs-DLQ --attribute-names QueueArn --query 'Attributes.QueueArn' --output text)
awslocal sqs create-queue --queue-name edi-orchestrator-jobs --attributes "{\"RedrivePolicy\":\"{\\\"deadLetterTargetArn\\\":\\\"$JOBS_DLQ_ARN\\\",\\\"maxReceiveCount\\\":\\\"3\\\"}\"}"

echo "LocalStack Initialization Complete."
2 changes: 2 additions & 0 deletions frontend/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"date-fns": "^4.4.0",
"lucide-react": "^1.21.0",
"next-themes": "^0.4.6",
"node-forge": "^1.4.0",
"oidc-client-ts": "^3.5.0",
"react": "^19.2.7",
"react-dom": "^19.2.7",
Expand All @@ -44,6 +45,7 @@
"devDependencies": {
"@tanstack/router-plugin": "^1.168.19",
"@types/node": "^24.13.2",
"@types/node-forge": "^1.3.14",
"@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.2",
Expand Down
19 changes: 19 additions & 0 deletions frontend/web/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions frontend/web/src/features/partners/api/IPartnersRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import type {
*/
export interface IPartnersRepository {
// Platform Trading Partners
deleteCertificateSecret(vaultRef: string): Promise<void>;
getPlatformPartners(): Promise<Partner[]>;
createPlatformPartner(payload: CreatePartnerPayload): Promise<Partner>;
updatePlatformPartner(id: string, payload: UpdatePartnerPayload): Promise<Partner>;
Expand All @@ -32,6 +33,7 @@ export interface IPartnersRepository {
// Certificates
exportCertificates(partnerId: string): Promise<CertificatesExport>;
rotateCertificates(partnerId: string, payload: RotateCertPayload): Promise<Partner>;
generateCertificate(as2Id: string): Promise<{ public_cert_pem: string; private_key_vault_ref: string }>;

// Tenant Partners
getTenantPartners(): Promise<Partner[]>;
Expand Down
17 changes: 15 additions & 2 deletions frontend/web/src/features/partners/api/partnerHooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,16 +127,22 @@ export function useUpdatePlatformPartnerMutation() {
);
}

export function useDeletePlatformPartner() {
export function useDeletePlatformPartnerMutation() {
const repo = useRepository();

return useToastMutation(
(id: string) => repo.deletePlatformPartner(id),
'Partner deleted.',
[partnersKeys.platformPartners()]
);
}

export function useDeleteCertificateSecretMutation() {
const repo = useRepository();
return useMutation({
mutationFn: (vaultRef: string) => repo.deleteCertificateSecret(vaultRef),
});
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// ─────────────────────────────────────────────
// Platform Partnership Mutations
// ─────────────────────────────────────────────
Expand Down Expand Up @@ -228,6 +234,13 @@ export function useRotateCertificatesMutation() {
);
}

export function useGenerateCertificateMutation() {
const repo = useRepository();
return useMutation({
mutationFn: (as2Id: string) => repo.generateCertificate(as2Id)
});
}

export function useTestSftpConnectionMutation() {
const repo = useRepository();
return useMutation({
Expand Down
11 changes: 11 additions & 0 deletions frontend/web/src/features/partners/api/partnersApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ class HttpPartnersRepository implements IPartnersRepository {
}

// ── Platform Trading Partners ──────────────
deleteCertificateSecret(vaultRef: string): Promise<void> {
return this.request(`/api/v1/platform/trading-partners/as2/certificates/secret?vault_ref=${encodeURIComponent(vaultRef)}`, { method: 'DELETE' });
}

getPlatformPartners(): Promise<Partner[]> {
return this.request('/api/v1/platform/trading-partners/as2/trading-partners');
}
Expand Down Expand Up @@ -124,6 +128,13 @@ class HttpPartnersRepository implements IPartnersRepository {
);
}

generateCertificate(as2Id: string): Promise<{ public_cert_pem: string; private_key_vault_ref: string }> {
return this.request(
`/api/v1/platform/trading-partners/as2/certificates/generate`,
{ method: 'POST', body: JSON.stringify({ as2_id: as2Id }) },
);
}

// ── Tenant Partners ────────────────────────
async getTenantPartners(): Promise<Partner[]> {
const data = await this.request<any[]>('/api/v1/trading-partners');
Expand Down
Loading
Loading