PyReverseETL 3.1.5__tar.gz → 3.1.7__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pyreverseetl-3.1.5 → pyreverseetl-3.1.7}/Cargo.lock +2 -2
- {pyreverseetl-3.1.5 → pyreverseetl-3.1.7}/Cargo.toml +1 -1
- {pyreverseetl-3.1.5 → pyreverseetl-3.1.7}/PKG-INFO +2 -2
- {pyreverseetl-3.1.5 → pyreverseetl-3.1.7}/README.md +1 -1
- {pyreverseetl-3.1.5 → pyreverseetl-3.1.7}/core/src/connectors/database_advanced.rs +48 -37
- pyreverseetl-3.1.7/core/src/dlq.rs +161 -0
- {pyreverseetl-3.1.5 → pyreverseetl-3.1.7}/core/src/executor.rs +172 -19
- {pyreverseetl-3.1.5 → pyreverseetl-3.1.7}/core/src/lib.rs +2 -0
- {pyreverseetl-3.1.5 → pyreverseetl-3.1.7}/pyproject.toml +1 -1
- {pyreverseetl-3.1.5 → pyreverseetl-3.1.7}/python/pyreverseetl/cli.py +29 -1
- {pyreverseetl-3.1.5 → pyreverseetl-3.1.7}/python/src/lib.rs +16 -1
- {pyreverseetl-3.1.5 → pyreverseetl-3.1.7}/LICENSE +0 -0
- {pyreverseetl-3.1.5 → pyreverseetl-3.1.7}/core/Cargo.toml +0 -0
- {pyreverseetl-3.1.5 → pyreverseetl-3.1.7}/core/benches/load_test.rs +0 -0
- {pyreverseetl-3.1.5 → pyreverseetl-3.1.7}/core/bin/pyreverseetl_dashboard.rs +0 -0
- {pyreverseetl-3.1.5 → pyreverseetl-3.1.7}/core/src/activation.rs +0 -0
- {pyreverseetl-3.1.5 → pyreverseetl-3.1.7}/core/src/adapters/alert_compat.rs +0 -0
- {pyreverseetl-3.1.5 → pyreverseetl-3.1.7}/core/src/adapters/error.rs +0 -0
- {pyreverseetl-3.1.5 → pyreverseetl-3.1.7}/core/src/adapters/http_client.rs +0 -0
- {pyreverseetl-3.1.5 → pyreverseetl-3.1.7}/core/src/adapters/hubspot.rs +0 -0
- {pyreverseetl-3.1.5 → pyreverseetl-3.1.7}/core/src/adapters/mapping.rs +0 -0
- {pyreverseetl-3.1.5 → pyreverseetl-3.1.7}/core/src/adapters/marketo.rs +0 -0
- {pyreverseetl-3.1.5 → pyreverseetl-3.1.7}/core/src/adapters/mod.rs +0 -0
- {pyreverseetl-3.1.5 → pyreverseetl-3.1.7}/core/src/adapters/oauth_manager.rs +0 -0
- {pyreverseetl-3.1.5 → pyreverseetl-3.1.7}/core/src/adapters/retry_policy.rs +0 -0
- {pyreverseetl-3.1.5 → pyreverseetl-3.1.7}/core/src/adapters/salesforce.rs +0 -0
- {pyreverseetl-3.1.5 → pyreverseetl-3.1.7}/core/src/adapters/schema_detect.rs +0 -0
- {pyreverseetl-3.1.5 → pyreverseetl-3.1.7}/core/src/adapters/webhook.rs +0 -0
- {pyreverseetl-3.1.5 → pyreverseetl-3.1.7}/core/src/cdc/change_detector.rs +0 -0
- {pyreverseetl-3.1.5 → pyreverseetl-3.1.7}/core/src/cdc/changelog.rs +0 -0
- {pyreverseetl-3.1.5 → pyreverseetl-3.1.7}/core/src/cdc/checkpoint.rs +0 -0
- {pyreverseetl-3.1.5 → pyreverseetl-3.1.7}/core/src/cdc/mod.rs +0 -0
- {pyreverseetl-3.1.5 → pyreverseetl-3.1.7}/core/src/connectors/config.rs +0 -0
- {pyreverseetl-3.1.5 → pyreverseetl-3.1.7}/core/src/connectors/connectors_db.rs +0 -0
- {pyreverseetl-3.1.5 → pyreverseetl-3.1.7}/core/src/connectors/destination.rs +0 -0
- {pyreverseetl-3.1.5 → pyreverseetl-3.1.7}/core/src/connectors/error.rs +0 -0
- {pyreverseetl-3.1.5 → pyreverseetl-3.1.7}/core/src/connectors/hdfs.rs +0 -0
- {pyreverseetl-3.1.5 → pyreverseetl-3.1.7}/core/src/connectors/mod.rs +0 -0
- {pyreverseetl-3.1.5 → pyreverseetl-3.1.7}/core/src/connectors/mysql.rs +0 -0
- {pyreverseetl-3.1.5 → pyreverseetl-3.1.7}/core/src/connectors/object_storage.rs +0 -0
- {pyreverseetl-3.1.5 → pyreverseetl-3.1.7}/core/src/connectors/postgres.rs +0 -0
- {pyreverseetl-3.1.5 → pyreverseetl-3.1.7}/core/src/connectors/rate_limiting.rs +0 -0
- {pyreverseetl-3.1.5 → pyreverseetl-3.1.7}/core/src/connectors/registry.rs +0 -0
- {pyreverseetl-3.1.5 → pyreverseetl-3.1.7}/core/src/connectors/saas.rs +0 -0
- {pyreverseetl-3.1.5 → pyreverseetl-3.1.7}/core/src/connectors/source.rs +0 -0
- {pyreverseetl-3.1.5 → pyreverseetl-3.1.7}/core/src/connectors/web.rs +0 -0
- {pyreverseetl-3.1.5 → pyreverseetl-3.1.7}/core/src/destination.rs +0 -0
- {pyreverseetl-3.1.5 → pyreverseetl-3.1.7}/core/src/entity.rs +0 -0
- {pyreverseetl-3.1.5 → pyreverseetl-3.1.7}/core/src/error.rs +0 -0
- {pyreverseetl-3.1.5 → pyreverseetl-3.1.7}/core/src/governance/cached_gate.rs +0 -0
- {pyreverseetl-3.1.5 → pyreverseetl-3.1.7}/core/src/governance/compliance_rules.rs +0 -0
- {pyreverseetl-3.1.5 → pyreverseetl-3.1.7}/core/src/governance/credentials.rs +0 -0
- {pyreverseetl-3.1.5 → pyreverseetl-3.1.7}/core/src/governance/mod.rs +0 -0
- {pyreverseetl-3.1.5 → pyreverseetl-3.1.7}/core/src/governance/quality_gate.rs +0 -0
- {pyreverseetl-3.1.5 → pyreverseetl-3.1.7}/core/src/governance/retry_policy.rs +0 -0
- {pyreverseetl-3.1.5 → pyreverseetl-3.1.7}/core/src/governance/schema_evolution.rs +0 -0
- {pyreverseetl-3.1.5 → pyreverseetl-3.1.7}/core/src/governance/statguardian_client.rs +0 -0
- {pyreverseetl-3.1.5 → pyreverseetl-3.1.7}/core/src/idempotency.rs +0 -0
- {pyreverseetl-3.1.5 → pyreverseetl-3.1.7}/core/src/lineage.rs +0 -0
- {pyreverseetl-3.1.5 → pyreverseetl-3.1.7}/core/src/observability/dashboard_launcher.rs +0 -0
- {pyreverseetl-3.1.5 → pyreverseetl-3.1.7}/core/src/observability/logs.rs +0 -0
- {pyreverseetl-3.1.5 → pyreverseetl-3.1.7}/core/src/observability/metrics.rs +0 -0
- {pyreverseetl-3.1.5 → pyreverseetl-3.1.7}/core/src/observability/metrics_server.rs +0 -0
- {pyreverseetl-3.1.5 → pyreverseetl-3.1.7}/core/src/observability/mod.rs +0 -0
- {pyreverseetl-3.1.5 → pyreverseetl-3.1.7}/core/src/observability/traces.rs +0 -0
- {pyreverseetl-3.1.5 → pyreverseetl-3.1.7}/core/src/pipeline/activation_pipeline.rs +0 -0
- {pyreverseetl-3.1.5 → pyreverseetl-3.1.7}/core/src/pipeline/backpressure.rs +0 -0
- {pyreverseetl-3.1.5 → pyreverseetl-3.1.7}/core/src/pipeline/latency_tracker.rs +0 -0
- {pyreverseetl-3.1.5 → pyreverseetl-3.1.7}/core/src/pipeline/mod.rs +0 -0
- {pyreverseetl-3.1.5 → pyreverseetl-3.1.7}/core/src/sources/kafka.rs +0 -0
- {pyreverseetl-3.1.5 → pyreverseetl-3.1.7}/core/src/sources/mod.rs +0 -0
- {pyreverseetl-3.1.5 → pyreverseetl-3.1.7}/core/src/sources/polling.rs +0 -0
- {pyreverseetl-3.1.5 → pyreverseetl-3.1.7}/core/src/sources/sync_config.rs +0 -0
- {pyreverseetl-3.1.5 → pyreverseetl-3.1.7}/core/src/statguardian.rs +0 -0
- {pyreverseetl-3.1.5 → pyreverseetl-3.1.7}/core/src/storage/mod.rs +0 -0
- {pyreverseetl-3.1.5 → pyreverseetl-3.1.7}/core/src/storage/repository.rs +0 -0
- {pyreverseetl-3.1.5 → pyreverseetl-3.1.7}/core/src/storage/schema.rs +0 -0
- {pyreverseetl-3.1.5 → pyreverseetl-3.1.7}/core/src/streaming/event_processor.rs +0 -0
- {pyreverseetl-3.1.5 → pyreverseetl-3.1.7}/core/src/streaming/event_schema.rs +0 -0
- {pyreverseetl-3.1.5 → pyreverseetl-3.1.7}/core/src/streaming/mod.rs +0 -0
- {pyreverseetl-3.1.5 → pyreverseetl-3.1.7}/core/src/streampdf.rs +0 -0
- {pyreverseetl-3.1.5 → pyreverseetl-3.1.7}/core/src/streamxl.rs +0 -0
- {pyreverseetl-3.1.5 → pyreverseetl-3.1.7}/core/src/sync.rs +0 -0
- {pyreverseetl-3.1.5 → pyreverseetl-3.1.7}/core/src/testing/connector_test.rs +0 -0
- {pyreverseetl-3.1.5 → pyreverseetl-3.1.7}/core/src/testing/harness.rs +0 -0
- {pyreverseetl-3.1.5 → pyreverseetl-3.1.7}/core/src/testing/metrics.rs +0 -0
- {pyreverseetl-3.1.5 → pyreverseetl-3.1.7}/core/src/testing/mock_http.rs +0 -0
- {pyreverseetl-3.1.5 → pyreverseetl-3.1.7}/core/src/testing/mod.rs +0 -0
- {pyreverseetl-3.1.5 → pyreverseetl-3.1.7}/core/src/testing/test_data.rs +0 -0
- {pyreverseetl-3.1.5 → pyreverseetl-3.1.7}/core/src/transformers/mod.rs +0 -0
- {pyreverseetl-3.1.5 → pyreverseetl-3.1.7}/core/src/transformers/spark.rs +0 -0
- {pyreverseetl-3.1.5 → pyreverseetl-3.1.7}/core/src/workflow.rs +0 -0
- {pyreverseetl-3.1.5 → pyreverseetl-3.1.7}/python/Cargo.toml +0 -0
- {pyreverseetl-3.1.5 → pyreverseetl-3.1.7}/python/pyreverseetl/__init__.py +0 -0
- {pyreverseetl-3.1.5 → pyreverseetl-3.1.7}/python/pyreverseetl/server.py +0 -0
|
@@ -3199,7 +3199,7 @@ dependencies = [
|
|
|
3199
3199
|
|
|
3200
3200
|
[[package]]
|
|
3201
3201
|
name = "pyreverseetl"
|
|
3202
|
-
version = "3.1.
|
|
3202
|
+
version = "3.1.7"
|
|
3203
3203
|
dependencies = [
|
|
3204
3204
|
"chrono",
|
|
3205
3205
|
"pyo3",
|
|
@@ -3210,7 +3210,7 @@ dependencies = [
|
|
|
3210
3210
|
|
|
3211
3211
|
[[package]]
|
|
3212
3212
|
name = "pyreverseetl-core"
|
|
3213
|
-
version = "3.1.
|
|
3213
|
+
version = "3.1.7"
|
|
3214
3214
|
dependencies = [
|
|
3215
3215
|
"async-trait",
|
|
3216
3216
|
"aws-config",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyReverseETL
|
|
3
|
-
Version: 3.1.
|
|
3
|
+
Version: 3.1.7
|
|
4
4
|
Classifier: Development Status :: 4 - Beta
|
|
5
5
|
Classifier: Intended Audience :: Developers
|
|
6
6
|
Classifier: Programming Language :: Python :: 3
|
|
@@ -103,7 +103,7 @@ repeated here to avoid implying vendor endorsement.
|
|
|
103
103
|
|
|
104
104
|
## Project status
|
|
105
105
|
|
|
106
|
-
**Beta.** Version 3.1.
|
|
106
|
+
**Beta.** Version 3.1.6. The Postgres/MySQL/S3/webhook paths are real and verified against
|
|
107
107
|
real services; the `web` source connector (MudFish-backed site crawling) is implemented
|
|
108
108
|
and unit-tested but not yet exercised end-to-end against a live site; the CRM/marketing
|
|
109
109
|
adapters are real but mock-verified only; several
|
|
@@ -80,7 +80,7 @@ repeated here to avoid implying vendor endorsement.
|
|
|
80
80
|
|
|
81
81
|
## Project status
|
|
82
82
|
|
|
83
|
-
**Beta.** Version 3.1.
|
|
83
|
+
**Beta.** Version 3.1.6. The Postgres/MySQL/S3/webhook paths are real and verified against
|
|
84
84
|
real services; the `web` source connector (MudFish-backed site crawling) is implemented
|
|
85
85
|
and unit-tested but not yet exercised end-to-end against a live site; the CRM/marketing
|
|
86
86
|
adapters are real but mock-verified only; several
|
|
@@ -222,27 +222,29 @@ impl super::DestinationConnector for AdvancedDatabaseDestination {
|
|
|
222
222
|
}
|
|
223
223
|
|
|
224
224
|
async fn test_connection(&self) -> crate::Result<ConnectionTest> {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
225
|
+
// Honest: this connector has no real driver, so it cannot connect.
|
|
226
|
+
Ok(ConnectionTest::failure(
|
|
227
|
+
&format!(
|
|
228
|
+
"{} destination is not implemented: no real connection was attempted",
|
|
229
|
+
self.config.database_type
|
|
230
|
+
),
|
|
231
|
+
Some("advanced database connector is a stub (RECHECK-054)".to_string()),
|
|
232
|
+
))
|
|
229
233
|
}
|
|
230
234
|
|
|
231
235
|
async fn write_record(&self, _record: &Record) -> crate::Result<()> {
|
|
232
|
-
|
|
233
|
-
|
|
236
|
+
Err(crate::Error::ConnectorError(format!(
|
|
237
|
+
"{} destination write is not implemented; refusing to report a write that did not happen",
|
|
238
|
+
self.config.database_type
|
|
239
|
+
)))
|
|
234
240
|
}
|
|
235
241
|
|
|
236
|
-
async fn write_batch(&self,
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
// - BULK_LOAD: write to S3/GCS, COPY into database
|
|
243
|
-
// 3. Handle partitioning for warehouses
|
|
244
|
-
// 4. Create clustering indexes for BigQuery
|
|
245
|
-
Ok(records.len())
|
|
242
|
+
async fn write_batch(&self, _records: &[Record]) -> crate::Result<usize> {
|
|
243
|
+
Err(crate::Error::ConnectorError(format!(
|
|
244
|
+
"{} destination batch write is not implemented; refusing to report {} rows written when none were",
|
|
245
|
+
self.config.database_type,
|
|
246
|
+
_records.len()
|
|
247
|
+
)))
|
|
246
248
|
}
|
|
247
249
|
|
|
248
250
|
async fn validate_records(&self, _records: &[Record]) -> crate::Result<()> {
|
|
@@ -283,38 +285,42 @@ impl super::SourceConnector for AdvancedDatabaseSource {
|
|
|
283
285
|
}
|
|
284
286
|
|
|
285
287
|
async fn test_connection(&self) -> crate::Result<ConnectionTest> {
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
288
|
+
// Honest: this connector has no real driver, so it cannot connect.
|
|
289
|
+
Ok(ConnectionTest::failure(
|
|
290
|
+
&format!(
|
|
291
|
+
"{} source is not implemented: no real connection was attempted",
|
|
292
|
+
self.config.database_type
|
|
293
|
+
),
|
|
294
|
+
Some("advanced database connector is a stub (RECHECK-054)".to_string()),
|
|
295
|
+
))
|
|
290
296
|
}
|
|
291
297
|
|
|
292
298
|
async fn detect_schema(&self) -> crate::Result<super::Schema> {
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
sample_records: vec![],
|
|
298
|
-
})
|
|
299
|
+
Err(crate::Error::ConnectorError(format!(
|
|
300
|
+
"{} schema detection is not implemented",
|
|
301
|
+
self.config.database_type
|
|
302
|
+
)))
|
|
299
303
|
}
|
|
300
304
|
|
|
301
305
|
async fn read_all(&self) -> crate::Result<Vec<Record>> {
|
|
302
|
-
|
|
303
|
-
|
|
306
|
+
Err(crate::Error::ConnectorError(format!(
|
|
307
|
+
"{} read is not implemented; refusing to report an empty result as a successful read",
|
|
308
|
+
self.config.database_type
|
|
309
|
+
)))
|
|
304
310
|
}
|
|
305
311
|
|
|
306
312
|
async fn read_batch(&self, _offset: u64, _limit: u64) -> crate::Result<Vec<Record>> {
|
|
307
|
-
|
|
308
|
-
|
|
313
|
+
Err(crate::Error::ConnectorError(format!(
|
|
314
|
+
"{} read_batch is not implemented",
|
|
315
|
+
self.config.database_type
|
|
316
|
+
)))
|
|
309
317
|
}
|
|
310
318
|
|
|
311
319
|
async fn read_incremental(&self, _last_value: &str) -> crate::Result<Vec<Record>> {
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
// SELECT * FROM table WHERE id > ? ORDER BY id LIMIT ?
|
|
317
|
-
Ok(vec![])
|
|
320
|
+
Err(crate::Error::ConnectorError(format!(
|
|
321
|
+
"{} read_incremental is not implemented",
|
|
322
|
+
self.config.database_type
|
|
323
|
+
)))
|
|
318
324
|
}
|
|
319
325
|
|
|
320
326
|
fn capabilities(&self) -> Vec<Capability> {
|
|
@@ -370,8 +376,13 @@ mod tests {
|
|
|
370
376
|
let dest = AdvancedDatabaseDestination { config };
|
|
371
377
|
assert_eq!(dest.name(), "Snowflake");
|
|
372
378
|
|
|
379
|
+
// The connector is a stub: it must report failure, not fake success.
|
|
373
380
|
let test = dest.test_connection().await.unwrap();
|
|
374
|
-
assert!(test.success);
|
|
381
|
+
assert!(!test.success);
|
|
382
|
+
assert!(test.message.contains("not implemented"));
|
|
383
|
+
|
|
384
|
+
// And a write must error rather than silently claim rows were written.
|
|
385
|
+
assert!(dest.write_batch(&[]).await.is_err());
|
|
375
386
|
}
|
|
376
387
|
|
|
377
388
|
#[tokio::test]
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
//! Local dead-letter queue (quarantine) for records that failed to write.
|
|
2
|
+
//!
|
|
3
|
+
//! Per the verified audit gap (RECHECK-018): a sync that partially fails left
|
|
4
|
+
//! the operator with only an aggregate `rows_failed` count -- there was no
|
|
5
|
+
//! record of *which* records failed, why, or their payload, so re-driving a
|
|
6
|
+
//! failed batch after a transient destination error (a rate limit, a 5xx, a
|
|
7
|
+
//! single malformed record) meant re-reading the whole source and guessing.
|
|
8
|
+
//!
|
|
9
|
+
//! `DeadLetterQueue` is a real, persisted (SQLite) quarantine: every record
|
|
10
|
+
//! that a destination rejected is stored with its run id, destination key,
|
|
11
|
+
//! the reason for rejection, and its exact payload, so a failed record can be
|
|
12
|
+
//! inspected and replayed deliberately instead of re-running the entire sync.
|
|
13
|
+
|
|
14
|
+
use rusqlite::{params, Connection};
|
|
15
|
+
use serde::Serialize;
|
|
16
|
+
use std::path::Path;
|
|
17
|
+
use std::sync::Mutex;
|
|
18
|
+
|
|
19
|
+
/// One quarantined record: what failed, where it was headed, and why.
|
|
20
|
+
#[derive(Debug, Clone, Serialize)]
|
|
21
|
+
pub struct DeadLetter {
|
|
22
|
+
pub run_id: String,
|
|
23
|
+
pub destination_key: String,
|
|
24
|
+
pub record_id: String,
|
|
25
|
+
pub reason: String,
|
|
26
|
+
/// The exact record payload (compact JSON) that was rejected.
|
|
27
|
+
pub payload: String,
|
|
28
|
+
pub failed_at: String,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
pub struct DeadLetterQueue {
|
|
32
|
+
conn: Mutex<Connection>,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
impl DeadLetterQueue {
|
|
36
|
+
/// Open (creating if absent) a real SQLite file as the quarantine store.
|
|
37
|
+
pub fn open(path: &Path) -> crate::Result<Self> {
|
|
38
|
+
let conn = Connection::open(path)?;
|
|
39
|
+
Self::from_connection(conn)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/// SQLite-backed but in-memory -- exercises the real SQL code path without
|
|
43
|
+
/// touching disk (tests).
|
|
44
|
+
#[cfg(test)]
|
|
45
|
+
pub fn in_memory() -> crate::Result<Self> {
|
|
46
|
+
let conn = Connection::open_in_memory()?;
|
|
47
|
+
Self::from_connection(conn)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
fn from_connection(conn: Connection) -> crate::Result<Self> {
|
|
51
|
+
conn.execute(
|
|
52
|
+
"CREATE TABLE IF NOT EXISTS dead_letters (
|
|
53
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
54
|
+
run_id TEXT NOT NULL,
|
|
55
|
+
destination_key TEXT NOT NULL,
|
|
56
|
+
record_id TEXT NOT NULL,
|
|
57
|
+
reason TEXT NOT NULL,
|
|
58
|
+
payload TEXT NOT NULL,
|
|
59
|
+
failed_at TEXT NOT NULL
|
|
60
|
+
)",
|
|
61
|
+
[],
|
|
62
|
+
)?;
|
|
63
|
+
conn.execute(
|
|
64
|
+
"CREATE INDEX IF NOT EXISTS idx_dead_letters_run ON dead_letters(run_id)",
|
|
65
|
+
[],
|
|
66
|
+
)?;
|
|
67
|
+
Ok(Self {
|
|
68
|
+
conn: Mutex::new(conn),
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/// Quarantine one failed record. Appends a new row every time (a record
|
|
73
|
+
/// that fails across several runs leaves a row per run), so the history of
|
|
74
|
+
/// repeated failures is preserved rather than overwritten.
|
|
75
|
+
#[allow(clippy::too_many_arguments)]
|
|
76
|
+
pub fn record(
|
|
77
|
+
&self,
|
|
78
|
+
run_id: &str,
|
|
79
|
+
destination_key: &str,
|
|
80
|
+
record_id: &str,
|
|
81
|
+
reason: &str,
|
|
82
|
+
payload: &str,
|
|
83
|
+
) -> crate::Result<()> {
|
|
84
|
+
let failed_at = chrono::Utc::now().to_rfc3339();
|
|
85
|
+
let conn = self.conn.lock().unwrap();
|
|
86
|
+
conn.execute(
|
|
87
|
+
"INSERT INTO dead_letters
|
|
88
|
+
(run_id, destination_key, record_id, reason, payload, failed_at)
|
|
89
|
+
VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
|
|
90
|
+
params![run_id, destination_key, record_id, reason, payload, failed_at],
|
|
91
|
+
)?;
|
|
92
|
+
Ok(())
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/// Every record quarantined by a single run, in insertion order.
|
|
96
|
+
pub fn list_for_run(&self, run_id: &str) -> crate::Result<Vec<DeadLetter>> {
|
|
97
|
+
let conn = self.conn.lock().unwrap();
|
|
98
|
+
let mut stmt = conn.prepare(
|
|
99
|
+
"SELECT run_id, destination_key, record_id, reason, payload, failed_at
|
|
100
|
+
FROM dead_letters WHERE run_id = ?1 ORDER BY id ASC",
|
|
101
|
+
)?;
|
|
102
|
+
let rows = stmt.query_map(params![run_id], |row| {
|
|
103
|
+
Ok(DeadLetter {
|
|
104
|
+
run_id: row.get(0)?,
|
|
105
|
+
destination_key: row.get(1)?,
|
|
106
|
+
record_id: row.get(2)?,
|
|
107
|
+
reason: row.get(3)?,
|
|
108
|
+
payload: row.get(4)?,
|
|
109
|
+
failed_at: row.get(5)?,
|
|
110
|
+
})
|
|
111
|
+
})?;
|
|
112
|
+
let mut out = Vec::new();
|
|
113
|
+
for row in rows {
|
|
114
|
+
out.push(row?);
|
|
115
|
+
}
|
|
116
|
+
Ok(out)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/// Total number of quarantined records across every run.
|
|
120
|
+
pub fn count(&self) -> crate::Result<u64> {
|
|
121
|
+
let conn = self.conn.lock().unwrap();
|
|
122
|
+
let count: i64 = conn.query_row("SELECT COUNT(*) FROM dead_letters", [], |row| row.get(0))?;
|
|
123
|
+
Ok(count as u64)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
#[cfg(test)]
|
|
128
|
+
mod tests {
|
|
129
|
+
use super::*;
|
|
130
|
+
|
|
131
|
+
#[test]
|
|
132
|
+
fn records_and_lists_failures_per_run() {
|
|
133
|
+
let dlq = DeadLetterQueue::in_memory().unwrap();
|
|
134
|
+
dlq.record("run-1", "webhook:x", "rec-1", "HTTP 429", "{\"a\":1}")
|
|
135
|
+
.unwrap();
|
|
136
|
+
dlq.record("run-1", "webhook:x", "rec-2", "HTTP 500", "{\"b\":2}")
|
|
137
|
+
.unwrap();
|
|
138
|
+
dlq.record("run-2", "webhook:x", "rec-3", "boom", "{\"c\":3}")
|
|
139
|
+
.unwrap();
|
|
140
|
+
|
|
141
|
+
let run1 = dlq.list_for_run("run-1").unwrap();
|
|
142
|
+
assert_eq!(run1.len(), 2);
|
|
143
|
+
assert_eq!(run1[0].record_id, "rec-1");
|
|
144
|
+
assert_eq!(run1[0].reason, "HTTP 429");
|
|
145
|
+
assert_eq!(run1[0].payload, "{\"a\":1}");
|
|
146
|
+
assert_eq!(run1[1].record_id, "rec-2");
|
|
147
|
+
|
|
148
|
+
assert_eq!(dlq.list_for_run("run-2").unwrap().len(), 1);
|
|
149
|
+
assert_eq!(dlq.count().unwrap(), 3);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
#[test]
|
|
153
|
+
fn repeated_failures_are_appended_not_overwritten() {
|
|
154
|
+
let dlq = DeadLetterQueue::in_memory().unwrap();
|
|
155
|
+
dlq.record("run-1", "webhook:x", "rec-1", "HTTP 429", "{}")
|
|
156
|
+
.unwrap();
|
|
157
|
+
dlq.record("run-2", "webhook:x", "rec-1", "HTTP 429", "{}")
|
|
158
|
+
.unwrap();
|
|
159
|
+
assert_eq!(dlq.count().unwrap(), 2);
|
|
160
|
+
}
|
|
161
|
+
}
|