| 326 | |
| 327 | @dataclass |
| 328 | class PgDisruption: |
| 329 | name: str |
| 330 | breakage: Callable |
| 331 | expected_error: str |
| 332 | fixage: Callable | None |
| 333 | |
| 334 | def run_test(self, c: Composition) -> None: |
| 335 | print(f"+++ Running disruption scenario {self.name}") |
| 336 | seed = random.randint(0, 256**4) |
| 337 | |
| 338 | c.up( |
| 339 | "postgres", |
| 340 | "materialized", |
| 341 | "clusterd", |
| 342 | Service("testdrive", idle=True), |
| 343 | ) |
| 344 | |
| 345 | with c.override( |
| 346 | Testdrive( |
| 347 | no_reset=True, |
| 348 | seed=seed, |
| 349 | entrypoint_extra=["--initial-backoff=1s", "--backoff-factor=0"], |
| 350 | ) |
| 351 | ): |
| 352 | self.populate(c) |
| 353 | self.breakage(c, seed) |
| 354 | self.assert_error(c, self.expected_error) |
| 355 | |
| 356 | if self.fixage: |
| 357 | self.fixage(c, seed) |
| 358 | self.assert_recovery(c) |
| 359 | |
| 360 | def populate(self, c: Composition) -> None: |
| 361 | # Create a source and a sink |
| 362 | c.testdrive(dedent(""" |
| 363 | > CREATE SECRET pgpass AS 'postgres' |
| 364 | > CREATE CONNECTION pg TO POSTGRES ( |
| 365 | HOST postgres, |
| 366 | DATABASE postgres, |
| 367 | USER postgres, |
| 368 | PASSWORD SECRET pgpass |
| 369 | ) |
| 370 | |
| 371 | $ postgres-execute connection=postgres://postgres:postgres@postgres |
| 372 | ALTER USER postgres WITH replication; |
| 373 | DROP SCHEMA IF EXISTS public CASCADE; |
| 374 | CREATE SCHEMA public; |
| 375 | |
| 376 | DROP PUBLICATION IF EXISTS mz_source; |
| 377 | CREATE PUBLICATION mz_source FOR ALL TABLES; |
| 378 | |
| 379 | CREATE TABLE source1 (f1 INTEGER PRIMARY KEY, f2 integer[]); |
| 380 | INSERT INTO source1 VALUES (1, NULL); |
| 381 | ALTER TABLE source1 REPLICA IDENTITY FULL; |
| 382 | INSERT INTO source1 VALUES (2, NULL); |
| 383 | |
| 384 | > CREATE SOURCE "pg_source" |
| 385 | FROM POSTGRES CONNECTION pg (PUBLICATION 'mz_source'); |