Skip to content

Commit 6810835

Browse files
starting dynamodb -> mysql/mariadb migration
1 parent d14dd48 commit 6810835

File tree

21 files changed

+1899
-13
lines changed

21 files changed

+1899
-13
lines changed

cmd/main/config/config.yaml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,25 @@ health-check:
1616
server:
1717
port: 8080
1818

19+
mysql:
20+
host: "${MYSQL_HOST}"
21+
port: ${MYSQL_PORT}
22+
user: "${MYSQL_USER}"
23+
password: "${MYSQL_PASSWORD}"
24+
database: "${MYSQL_DATABASE}"
25+
1926
outbox:
2027
table-name: "${EMAIL_OUTBOX_TABLE}"
2128

2229
pipeline:
2330
interval: ${PIPELINE_INTERVAL}
2431

32+
pipelines:
33+
dynamodb:
34+
enabled: ${DYNAMODB_PIPELINES_ENABLED}
35+
mysql:
36+
enabled: false
37+
2538
smtp:
2639
host: "${SMTP_HOST}"
2740
port: ${SMTP_PORT}

compose.yml

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,13 @@ services:
2525
ATTACHMENTS_BASE_PATH: 'testdata/attachments'
2626
EMAIL_OUTBOX_TABLE: 'Outbox'
2727
EML_STORAGE_PATH: 'testdata/.out/eml'
28+
MYSQL_HOST: '127.0.0.1'
29+
MYSQL_PORT: '3306'
30+
MYSQL_USER: 'root'
31+
MYSQL_PASSWORD: 'test'
32+
MYSQL_DATABASE: 'mailculator_test'
33+
DYNAMODB_PIPELINES_ENABLED: 'true'
34+
MYSQL_PIPELINES_ENABLED: 'true'
2835
command: >
2936
sh -c "go mod tidy &&
3037
./scripts/coverage.sh unit &&
@@ -153,3 +160,64 @@ services:
153160
AWS_SECRET_ACCESS_KEY: 'local'
154161
networks:
155162
- mailculator-processor-deployments-net
163+
164+
mysql: &mysql-base
165+
profiles:
166+
- 'none'
167+
image: mariadb:10.11
168+
environment:
169+
MARIADB_ROOT_PASSWORD: 'test'
170+
MARIADB_DATABASE: 'mailculator_test'
171+
volumes:
172+
- './docker/mysql/migrations:/docker-entrypoint-initdb.d/migrations:ro'
173+
- './docker/mysql/init.sh:/docker-entrypoint-initdb.d/zzz-init.sh:ro'
174+
healthcheck:
175+
test: ['CMD-SHELL', 'test -f /var/lib/mysql/zz-finish && mysqladmin ping -h localhost -ptest']
176+
interval: 2s
177+
timeout: 5s
178+
retries: 30
179+
ports:
180+
- '127.0.0.1:3306:3306'
181+
networks:
182+
- mailculator-processor-deployments-net
183+
184+
mysql-test:
185+
<<: *mysql-base
186+
container_name: mailculator_processor_mysql_test
187+
profiles:
188+
- 'test-deps'
189+
190+
mysql-devcontainer:
191+
<<: *mysql-base
192+
container_name: mailculator_processor_mysql_devcontainer
193+
profiles:
194+
- 'devcontainer-deps'
195+
196+
wait-for-mysql-test:
197+
container_name: mailculator_processor_wait_for_mysql_test
198+
profiles:
199+
- 'test-deps'
200+
image: golang:1.25-alpine
201+
command: ['echo', 'Service mysql-test is ready']
202+
networks:
203+
- mailculator-processor-deployments-net
204+
depends_on:
205+
mysql-test:
206+
condition: service_healthy
207+
208+
phpmyadmin:
209+
container_name: mailculator_processor_phpmyadmin_devcontainer
210+
profiles:
211+
- 'devcontainer-deps'
212+
image: phpmyadmin:latest
213+
ports:
214+
- '127.0.0.1:9003:80'
215+
environment:
216+
PMA_HOST: 'mysql-devcontainer'
217+
PMA_USER: 'root'
218+
PMA_PASSWORD: 'test'
219+
networks:
220+
- mailculator-processor-deployments-net
221+
depends_on:
222+
mysql-devcontainer:
223+
condition: service_healthy

docker/mysql/init.sh

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
#!/bin/bash
2+
set -e
3+
4+
echo "Running MariaDB migrations..."
5+
6+
for f in /docker-entrypoint-initdb.d/migrations/*.sql; do
7+
echo "Executing $f..."
8+
mysql -u root -p"$MARIADB_ROOT_PASSWORD" "$MARIADB_DATABASE" < "$f"
9+
done
10+
11+
echo "Migrations completed. Creating finish marker..."
12+
touch /var/lib/mysql/zz-finish
13+
14+
echo "MariaDB initialization complete."
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
CREATE TABLE IF NOT EXISTS emails (
2+
id CHAR(36) PRIMARY KEY,
3+
status ENUM(
4+
'ACCEPTED','INTAKING','READY','PROCESSING',
5+
'SENT','FAILED','INVALID',
6+
'CALLING-SENT-CALLBACK','CALLING-FAILED-CALLBACK',
7+
'SENT-ACKNOWLEDGED','FAILED-ACKNOWLEDGED'
8+
) NOT NULL,
9+
eml_file_path VARCHAR(500),
10+
payload_file_path VARCHAR(500),
11+
reason TEXT,
12+
version INT NOT NULL DEFAULT 1,
13+
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
14+
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
15+
16+
INDEX idx_status (status),
17+
INDEX idx_status_updated (status, updated_at)
18+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
CREATE TABLE IF NOT EXISTS email_statuses (
2+
id BIGINT AUTO_INCREMENT PRIMARY KEY,
3+
email_id CHAR(36) NOT NULL,
4+
status VARCHAR(50) NOT NULL,
5+
reason TEXT,
6+
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
7+
8+
INDEX idx_email_id (email_id),
9+
FOREIGN KEY (email_id) REFERENCES emails(id) ON DELETE CASCADE
10+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

docs/database.md

Lines changed: 99 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ type Email struct {
1717
UpdatedAt string // Timestamp RFC3339 dell'ultimo aggiornamento
1818
Reason string // Motivo dell'ultimo stato
1919
TTL *int64 // Timestamp Unix in secondi per DynamoDB TTL (nil se non presente)
20+
Version int // Versione per optimistic locking (MySQL) o 0 (DynamoDB)
2021
}
2122
```
2223

@@ -34,7 +35,7 @@ DynamoDB TTL è configurato per eliminare automaticamente i record obsoleti. L'a
3435
ttl := time.Now().Add(7 * 24 * time.Hour).Unix()
3536
```
3637

37-
## Pattern di Versionamento
38+
## Pattern di Versionamento (DynamoDB)
3839

3940
### Status Meta
4041
- **Costante**: `StatusMeta = "_META"`
@@ -63,6 +64,101 @@ INSERT INTO "table" VALUE {'Id': ?, 'Status': ?, 'Attributes': ?, 'TTL': ?}
6364

6465
**Nota**: Il TTL viene sempre sincronizzato tra il record _META e i record di stato. Quando un TTL è presente, viene impostato sia alla radice del record _META che alla radice del nuovo record di stato.
6566

67+
## Unificazione dei Tipi
68+
69+
Entrambi i backend (DynamoDB e MySQL) utilizzano ora lo stesso tipo `Email` con tutti i campi necessari:
70+
71+
- **DynamoDB**: `Version = 0` (non usa locking basato su versione)
72+
- **MySQL**: `Version` popolato dal database per optimistic locking
73+
- **TTL**: Presente per DynamoDB, `nil` per MySQL (non supportato)
74+
75+
Questo approccio elimina la duplicazione dei tipi e semplifica l'architettura.
76+
77+
---
78+
79+
## MySQL Schema
80+
81+
### Tabella `emails`
82+
Tabella principale per la gestione delle email.
83+
84+
```sql
85+
CREATE TABLE IF NOT EXISTS emails (
86+
id CHAR(36) PRIMARY KEY,
87+
status ENUM(
88+
'ACCEPTED','INTAKING','READY','PROCESSING',
89+
'SENT','FAILED','INVALID',
90+
'CALLING-SENT-CALLBACK','CALLING-FAILED-CALLBACK',
91+
'SENT-ACKNOWLEDGED','FAILED-ACKNOWLEDGED'
92+
) NOT NULL,
93+
eml_file_path VARCHAR(500),
94+
payload_file_path VARCHAR(500),
95+
reason TEXT,
96+
version INT NOT NULL DEFAULT 1,
97+
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
98+
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
99+
100+
INDEX idx_status (status),
101+
INDEX idx_status_updated (status, updated_at)
102+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
103+
```
104+
105+
**Nota**: A differenza di DynamoDB, MySQL non supporta TTL nativo. La pulizia dei record obsoleti deve essere gestita esternamente.
106+
107+
### Tabella `email_statuses`
108+
Tabella per lo storico dei cambi di stato (history).
109+
110+
```sql
111+
CREATE TABLE IF NOT EXISTS email_statuses (
112+
id BIGINT AUTO_INCREMENT PRIMARY KEY,
113+
email_id CHAR(36) NOT NULL,
114+
status VARCHAR(50) NOT NULL,
115+
reason TEXT,
116+
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
117+
118+
INDEX idx_email_id (email_id),
119+
FOREIGN KEY (email_id) REFERENCES emails(id) ON DELETE CASCADE
120+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
121+
```
122+
123+
### Optimistic Locking (MySQL)
124+
MySQL utilizza optimistic locking basato su:
125+
- Campo `Version` nel tipo `Email` per tracciare le modifiche
126+
- Campo `status` per validare la transizione di stato
127+
128+
Ogni update incrementa la versione e verifica lo stato atteso:
129+
```sql
130+
UPDATE emails
131+
SET status = ?, reason = ?, version = version + 1
132+
WHERE id = ? AND status = ?
133+
```
134+
135+
Se `affected_rows = 0`, l'operazione restituisce `ErrLockNotAcquired`.
136+
137+
**Nota**: DynamoDB non usa version-based locking, quindi restituisce sempre `Version = 0`.
138+
139+
### Query con SKIP LOCKED
140+
Le query di lettura utilizzano `FOR UPDATE SKIP LOCKED` per:
141+
- Evitare blocchi su righe già in uso da altri worker
142+
- Migliorare il throughput in scenari con più processori concorrenti
143+
144+
```sql
145+
SELECT id, status, eml_file_path, payload_file_path, reason, version, updated_at
146+
FROM emails
147+
WHERE status = ?
148+
ORDER BY updated_at ASC
149+
LIMIT ?
150+
FOR UPDATE SKIP LOCKED
151+
```
152+
153+
### Transazioni
154+
Le operazioni di update e insert history sono eseguite in transazione per garantire atomicità:
155+
1. `BEGIN`
156+
2. `UPDATE emails ...`
157+
3. `INSERT INTO email_statuses ...`
158+
4. `COMMIT` (o `ROLLBACK` in caso di errore)
159+
160+
---
161+
66162
## Stati Disponibili
67163
- `ACCEPTED` - Email accettato, in attesa di intake
68164
- `INTAKING` - Email in fase di elaborazione intake
@@ -76,7 +172,7 @@ INSERT INTO "table" VALUE {'Id': ?, 'Status': ?, 'Attributes': ?, 'TTL': ?}
76172
- `SENT-ACKNOWLEDGED` - Callback per email inviato completato
77173
- `FAILED-ACKNOWLEDGED` - Callback per email fallito completato
78174

79-
## Query Pattern
175+
## Query Pattern (DynamoDB)
80176

81177
### Query per Stato
82178
```sql
@@ -90,7 +186,7 @@ WHERE Status=? AND Attributes.Latest =?
90186
- Utilizza `NextToken` di DynamoDB per paginazione automatica
91187
- Interrompe quando raggiunge il limite di 25 record
92188

93-
## PartiQL Operations
189+
## PartiQL Operations (DynamoDB)
94190

95191
### ExecuteStatement
96192
Utilizzato per query con parametri e paginazione.

docs/error-handling.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,32 @@ Il sistema effettua retry automatico per le seguenti eccezioni DynamoDB:
1919
- **Formula**: Durata casuale tra 0 e min(2^attempt * base_delay, max_delay)
2020

2121

22+
## Retry MySQL
23+
24+
### Condizioni di Retry
25+
Il sistema effettua retry automatico per i seguenti errori MySQL:
26+
- `1205` - Lock wait timeout exceeded
27+
- `1213` - Deadlock found when trying to get lock
28+
- `1040` - Too many connections
29+
- `1203` - User already has more than max_user_connections active connections
30+
- `driver.ErrBadConn` - Connessione al database persa
31+
32+
### Errori NON Soggetti a Retry
33+
- `ErrLockNotAcquired` - Conflitto di lock ottimistico (il record è stato modificato da un altro processo)
34+
35+
### Backoff Strategy
36+
- **Max Attempts**: 8 tentativi
37+
- **Base Delay**: 30 millisecondi
38+
- **Max Delay**: 1 secondo
39+
- **Formula**: Durata casuale tra 0 e min(2^attempt * base_delay, max_delay)
40+
41+
### Transazioni
42+
Le operazioni MySQL (Update, Ready, Create) sono eseguite in transazione:
43+
- In caso di errore, viene eseguito automaticamente il rollback
44+
- In caso di successo, viene eseguito il commit
45+
- Gli errori transitori sono gestiti con retry (la transazione viene ritentata dall'inizio)
46+
47+
2248
## Retry Callback HTTP
2349

2450
### Condizioni di Retry
@@ -43,6 +69,7 @@ Quando il callback HTTP fallisce dopo tutti i retry:
4369
Quando non riesce ad acquisire il lock di processamento:
4470
- Operazione saltata
4571
- Log warning: "failed to acquire processing lock"
72+
- **Nessun retry**: `ErrLockNotAcquired` indica che un altro worker sta già processando il record
4673

4774
## Context Cancellation
4875
Tutti i retry rispettano il context cancellation:

0 commit comments

Comments
 (0)