GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — feldera/python/feldera/testutils.py at faster-parser · feldera/feldera

Expand file tree

/

testutils.py

Copy path
More file actions
More file actions

Latest commit

 

History

History
History
681 lines (544 loc) · 24.3 KB
/

testutils.py

Copy path

File metadata and controls

681 lines (544 loc) · 24.3 KB
Raw
Copy raw file
Download raw file
Open symbols panel
Edit and raw actions
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
"Utility functions for writing tests against a Feldera instance."
import logging
import os
import platform
import re
import time
import json
import unittest
import warnings
from pathlib import Path
from typing import List, Optional, cast
from datetime import datetime
from feldera._long_operation_warning import LongOperationWarning
from feldera.enums import CompilationProfile
from feldera.pipeline import Pipeline
from feldera.pipeline_builder import PipelineBuilder
from feldera.runtime_config import Resources, RuntimeConfig
from feldera.rest import FelderaClient, RetryConfig
from feldera.rest._helpers import requests_verify_from_env
logger = logging.getLogger(__name__)
API_KEY = os.environ.get("FELDERA_API_KEY")
# OIDC authentication support
def _get_oidc_token():
"""Get OIDC token if environment is configured, otherwise return None"""
try:
from feldera.testutils_oidc import get_oidc_test_helper
oidc_helper = get_oidc_test_helper()
if oidc_helper is not None:
return oidc_helper.obtain_access_token()
except ImportError:
pass
return None
def _get_effective_api_key():
"""Get effective API key - OIDC token takes precedence over static API key"""
oidc_token = _get_oidc_token()
return oidc_token if oidc_token else API_KEY
def feldera_bearer_token() -> Optional[str]:
"""The bearer token to send with a request issued now.
A configured OIDC login flow wins, then the token file named by
FELDERA_OIDC_TOKEN_FILE, which feldera/oidc-auth-action keeps current for
the life of a CI job. Then the static API key, and None when nothing is
configured, which is how a local instance without authentication runs.
Callers that build their own requests resolve this per request: a CI token
expires well inside a test run, and one read at import time starts
returning 401 partway through. Reading per request is not by itself
enough, because the file can hold an expired token when its refresher is
late; `RetryConfig.expired_token_wait_seconds` covers that gap for callers
that hand the SDK this function rather than its result.
"""
if os.environ.get("OIDC_TEST_ISSUER"):
return _get_effective_api_key()
token_file = os.environ.get("FELDERA_OIDC_TOKEN_FILE")
if token_file:
return Path(token_file).read_text().strip()
return API_KEY
BASE_URL = os.environ.get("FELDERA_HOST") or "http://localhost:8080"
FELDERA_REQUESTS_VERIFY = requests_verify_from_env()
FELDERA_TEST_NUM_WORKERS = int(os.environ.get("FELDERA_TEST_NUM_WORKERS", "8"))
FELDERA_TEST_NUM_HOSTS = int(os.environ.get("FELDERA_TEST_NUM_HOSTS", "1"))
class _LazyClient:
"Construct the FelderaClient only when accessed as opposed to when imported."
__slots__ = ("_client",)
def __init__(self):
self._client = None
def _ensure(self):
if self._client is None:
# A CI token expires inside a run, so where one refreshes the SDK
# gets the resolver itself: it re-resolves per request and retries
# once on 401. Elsewhere the credential is fixed for the process.
self._client = FelderaClient(
connection_timeout=10,
# Shared CI instances see infrastructure churn (node
# replacement, pipeline pod rescheduling) that outlasts the
# default attempt-based retry budget of ~14 seconds. Retry on
# a wall-clock budget sized to ride out a node replacement.
# CI only: `enterprise_only` gates call `get_config` at
# import time, and a 5-minute retry against an instance that
# is not running would hang local test collection.
retry_config=(
RetryConfig(
deadline_seconds=300.0,
# A late refresh leaves the token file holding an
# expired token, and every worker then fails at once
# for as long as the gap lasts. Waiting it out costs a
# couple of minutes; not waiting costs the whole job,
# because these suites run under `--maxfail=1`.
# Only a provably expired token waits, so a job with
# no credential at all still fails immediately.
expired_token_wait_seconds=150.0,
)
if os.environ.get("CI")
else RetryConfig()
),
api_key=(
feldera_bearer_token
if os.environ.get("FELDERA_OIDC_TOKEN_FILE")
else feldera_bearer_token()
),
)
return self._client
def __getattr__(self, name):
return getattr(self._ensure(), name)
def __call__(self, *a, **kw) -> FelderaClient:
return self._ensure()
TEST_CLIENT = cast(FelderaClient, _LazyClient())
# SQL index definition.
class IndexSpec:
def __init__(self, name: str, columns: List[str]):
self.name = name
self.columns = columns
def __repr__(self):
return f"IndexSpec(name={self.name!r},columns={self.columns!r})"
class ViewSpec:
"""
SQL view definition consisting of a query that can run in Feldera or
datafusion, optional connector spec and aux SQL statements, e.g., indexes
and lateness clauses following view definition.
"""
def __init__(
self,
name: str,
query: str,
indexes: List[IndexSpec] = [],
connectors: Optional[str] = None,
aux: Optional[str] = None,
expected_hash: Optional[str] = None,
):
if not isinstance(query, str):
raise TypeError("query must be a string")
self.name = name
self.query = query
self.connectors = connectors
self.indexes = indexes
self.aux = aux
self.expected_hash = expected_hash
def __repr__(self):
return f"ViewSpec(name={self.name!r}, query={self.query!r}, indexes={self.indexes!r}, connectors={self.connectors!r}, aux={self.aux!r}, expected_hash={self.expected_hash!r})"
def clone(self):
return ViewSpec(
self.name,
self.query,
self.indexes,
self.connectors,
self.aux,
self.expected_hash,
)
def clone_with_name(self, name: str):
return ViewSpec(name, self.query, self.indexes, self.connectors, self.aux)
def sql(self) -> str:
sql = ""
if self.connectors:
with_clause = f"\nwith('connectors' = '{self.connectors}')\n"
else:
with_clause = ""
sql += (
f"create materialized view {self.name}{with_clause} as\n{self.query};\n\n"
)
for index in self.indexes:
columns = ",".join(index.columns)
sql += f"create index {index.name} on {self.name}({columns});\n"
if self.aux:
sql += f"{self.aux}\n"
sql += "\n"
return sql
def log(*args, **kwargs):
"""Print like built-in print(), but prefix each line with current time."""
prefix = datetime.now().strftime("[%Y-%m-%d %H:%M:%S]")
print(prefix, *args, **kwargs)
# Cap on rows logged when a view validation fails. A failing TPC-H or test_now run
# can otherwise emit tens of megabytes on a single line, which blows up CI log capture.
_ROW_DIFF_LOG_LIMIT = 20
def _log_row_diff(label: str, rows: list) -> None:
if not rows:
return
log(
f"{label} ({len(rows)} rows; showing first {min(len(rows), _ROW_DIFF_LOG_LIMIT)}):"
)
for row in rows[:_ROW_DIFF_LOG_LIMIT]:
log(json.dumps(row, default=str))
def suite_tag() -> str:
"""Identifier of the test suite this process belongs to, or "" outside CI.
CI runs `python` and `python-multihost` concurrently against one Feldera
instance and one MinIO bucket. Only `python-multihost` sets
FELDERA_TEST_TAG_SUFFIX (to "m"), which is enough to keep anything the two
would otherwise share disjoint.
"""
return os.getenv("FELDERA_TEST_TAG_SUFFIX", "")
# A pipeline name becomes a Kubernetes label value, which the platform caps at
# 63 characters. Stay a character inside that.
PIPELINE_NAME_MAX_LEN = 62
def unique_pipeline_name(base_name: str) -> str:
"""
In CI, multiple tests of different runs can run against the same Feldera instance, we
make sure the pipeline names they use are unique by prepending `run_` and the first 5
characters of the commit SHA or 'local' if not in CI. The suite tag distinguishes
test suites of the same commit running concurrently against one instance.
Tests also name SQL relations and Avro records after the pipeline. The `run_`
prefix keeps those names valid when the commit SHA starts with a digit.
"""
ci_tag = "run_" + os.getenv("GITHUB_SHA", "local")[:5] + suite_tag()
name = f"{ci_tag}_{base_name}"
# Fail here with a clear message rather than letting provisioning hit a
# cryptic 422.
assert len(name) <= PIPELINE_NAME_MAX_LEN, (
f"Generated pipeline name '{name}' is {len(name)} chars, exceeding the "
f"{PIPELINE_NAME_MAX_LEN}-char limit; shorten the test name '{base_name}' to at "
f"most {PIPELINE_NAME_MAX_LEN - len(ci_tag) - 1} chars."
)
return name
def variant_pipeline_name(pipeline_name: str, variant: str) -> str:
"""Name the pipeline a test creates for one `variant` of itself.
`unique_pipeline_name` bounds only the name it returns, so a test that
appends a variant to that name leaves the bound behind. The length it lands
on also depends on the suite: `python-multihost` stamps a tag one character
longer than `python` does, so the same test can fit in one suite and
overflow in the other. Check the whole name here, where the test that built
it is still named, instead of at pipeline creation.
"""
name = f"{pipeline_name}_{variant}"
assert len(name) <= PIPELINE_NAME_MAX_LEN, (
f"Pipeline name '{name}' is {len(name)} chars, exceeding the "
f"{PIPELINE_NAME_MAX_LEN}-char limit; shorten the test name behind "
f"'{pipeline_name}' by {len(name) - PIPELINE_NAME_MAX_LEN} chars, or shorten "
f"the variant '{variant}'."
)
return name
# Teardown must not block on one wedged pipeline: the SDK otherwise polls for
# `Stopped` forever, and when the runner finally kills the job every pipeline
# the run started keeps consuming compute on the shared instance.
RECLAIM_TIMEOUT_SECONDS = 60.0
def reclaim_pipeline(name: str, delete: bool = False) -> List[str]:
"""Force-stop `name`, clear its storage, and optionally delete it.
Every step runs even when the one before it raised: a pipeline that never
reports `Stopped` can still release its storage, and abandoning the rest on
the first error leaves them running on a shared instance. Returns one
message per failed step, empty when the pipeline is fully reclaimed, so the
caller decides whether a cleanup failure is worth failing a test over.
"""
failures = []
try:
TEST_CLIENT.stop_pipeline(name, force=True, timeout_s=RECLAIM_TIMEOUT_SECONDS)
except Exception as error:
failures.append(f"{name}: stop: {error}")
try:
TEST_CLIENT.clear_storage(name, timeout_s=RECLAIM_TIMEOUT_SECONDS)
except Exception as error:
failures.append(f"{name}: clear storage: {error}")
if delete:
try:
TEST_CLIENT.delete_pipeline(name)
except Exception as error:
failures.append(f"{name}: delete: {error}")
return failures
def enterprise_only(fn):
fn._enterprise_only = True
return unittest.skipUnless(
TEST_CLIENT.get_config().edition.is_enterprise(),
f"{fn.__name__} is enterprise only, skipping",
)(fn)
def single_host_only(fn):
fn._single_host_only = True
return unittest.skipUnless(
FELDERA_TEST_NUM_HOSTS == 1,
f"multihost not yet supported for {fn.__name__}, skipping",
)(fn)
def skip_on_arm64(fn):
"""Skip the test on Linux aarch64.
Some native Python wheels (e.g. `deltalake`) bundle a jemalloc built
for 4 KB pages and abort on import on Ubuntu's 64 KB-page aarch64
kernel used by some CI runners. Tests that depend on such wheels can
use this decorator to opt out of running there.
"""
fn._skip_on_arm64 = True
return unittest.skipIf(
platform.machine() == "aarch64",
f"{fn.__name__} skipped on aarch64 (incompatible native wheel)",
)(fn)
def datafusionize(query: str) -> str:
sort_array_pattern = re.compile(re.escape("SORT_ARRAY"), re.IGNORECASE)
truncate_pattern = re.compile(re.escape("TRUNCATE"), re.IGNORECASE)
timestamp_trunc_pattern = re.compile(
r"TIMESTAMP_TRUNC\s*\(\s*MAKE_TIMESTAMP\s*\(\s*([^)]+)\s*\)\s*,\s*([A-Z]+)\s*\)",
re.IGNORECASE,
)
result = sort_array_pattern.sub("array_sort", query)
result = truncate_pattern.sub("trunc", result)
result = timestamp_trunc_pattern.sub(r"DATE_TRUNC('\2', TO_TIMESTAMP(\1))", result)
return result
def validate_view(pipeline: Pipeline, view: ViewSpec):
log(f"Validating view '{view.name}'")
# We have two modes to verify the view, either we run the same SQL as the view against datafusion
# by `datafusionizing` the query, or a weaker form where we pass a hash of what the result
# should look like and check that the hash hasn't changed
if view.expected_hash:
view_query = f"select * from {view.name}"
computed_hash = pipeline.query_hash(view_query)
if computed_hash != view.expected_hash:
raise AssertionError(
f"View {view.name} hash {computed_hash} was but expected hash {view.expected_hash}"
)
else:
# TODO: count records
view_query = datafusionize(view.query)
try:
extra_rows = list(
pipeline.query(f"(select * from {view.name}) except ({view_query})")
)
missing_rows = list(
pipeline.query(f"({view_query}) except (select * from {view.name})")
)
_log_row_diff(
"Extra rows in Feldera output, but not in the ad hoc query output",
extra_rows,
)
_log_row_diff(
"Extra rows in the ad hoc query output, but not in Feldera output",
missing_rows,
)
except Exception as e:
log(f"Error querying view '{view.name}': {e}")
log(f"Ad-hoc Query: {view_query}")
raise
if extra_rows or missing_rows:
raise AssertionError(f"Validation failed for view {view.name}")
def generate_program(tables: dict, views: List[ViewSpec]) -> str:
sql = ""
for table_sql in tables.values():
sql += f"{table_sql}\n"
for view in views:
sql += view.sql()
return sql
def build_pipeline(
pipeline_name: str,
tables: dict,
views: List[ViewSpec],
resources: Optional[Resources] = None,
dev_tweaks: Optional[dict] = None,
datafusion_memory_mb: Optional[int] = None,
udf_rust: str = "",
udf_toml: str = "",
) -> Pipeline:
"""Create or replace a workload test pipeline from its tables and views.
Workload tests build their pipelines here instead of calling
`PipelineBuilder` directly, so that the CI settings it applies, such as
the provisioning timeout, reach every one of them.
:param pipeline_name: Name of the pipeline to create or replace.
:param tables: SQL fragments that precede the views, keyed by name; see
`generate_program`.
:param views: Views to define after the tables.
:param resources: Resource requests and limits, or None for the
instance's defaults.
:param dev_tweaks: Developer tweaks for the runtime, or None.
:param datafusion_memory_mb: Size of the pool for ad hoc queries, or None
for the default share of the memory budget.
:param udf_rust: Rust source of the program's user-defined functions, or
"" if it has none.
:param udf_toml: Cargo dependencies of those functions, or "" if they
need none.
:return: The pipeline, created but not yet started.
"""
sql = generate_program(tables, views)
with warnings.catch_warnings():
# The older spelling of the timeout, so that a deployment from before
# the current names reads it too; it warns, and the warning is noise
# here. TODO: write `provisioning_timeout="5m"` once no such
# deployment is tested against any more.
warnings.simplefilter("ignore", DeprecationWarning)
runtime_config = RuntimeConfig(
# Covers node auto-provisioning: a pipeline that needs a fresh
# node shape waits for node boot plus image pull, and parallel
# test workers can request several fresh nodes at once.
provisioning_timeout_secs=300,
resources=resources,
workers=FELDERA_TEST_NUM_WORKERS,
hosts=FELDERA_TEST_NUM_HOSTS,
dev_tweaks=dev_tweaks,
datafusion_memory_mb=datafusion_memory_mb,
)
pipeline = PipelineBuilder(
TEST_CLIENT,
pipeline_name,
sql=sql,
udf_rust=udf_rust,
udf_toml=udf_toml,
compilation_profile=CompilationProfile.OPTIMIZED,
runtime_config=runtime_config,
).create_or_replace()
return pipeline
def validate_outputs(pipeline: Pipeline, tables: dict, views: List[ViewSpec]):
for table in tables.keys():
row_count = list(pipeline.query(f"select count(*) from {table}"))
log(f"Table '{table}' count(*):\n{row_count}")
for view in views:
validate_view(pipeline, view)
def check_end_of_input(pipeline: Pipeline) -> bool:
return all(
input_endpoint.metrics.end_of_input
for input_endpoint in pipeline.stats().inputs
)
def wait_end_of_input(pipeline: Pipeline, timeout_s: Optional[int] = None):
"""Waits until every input connector has read its source to the end.
End of input is not completion: a connector reports it once it has read its
last record, and records it has already read may still be waiting to reach
the circuit. `Pipeline.wait_for_completion` waits for those to be processed
as well; this weaker wait is for the cases that cannot reach completion,
such as an open transaction.
"""
start_time = time.monotonic()
# Reused by the warning message below so a stalled ingest can be told
# apart from a slow one; check_end_of_input() would fetch and discard it.
latest_stats = None
wait_for_input_end = LongOperationWarning(
logger,
lambda elapsed: (
f"still waiting for end of input on pipeline {pipeline.name}, "
f"waited {elapsed:.1f} seconds ({latest_stats.global_metrics.progress_summary()})"
),
lambda elapsed: (
f"end of input reached on pipeline {pipeline.name} "
f"after {elapsed:.1f} seconds"
),
)
while True:
latest_stats = pipeline.stats()
if all(
input_endpoint.metrics.end_of_input
for input_endpoint in latest_stats.inputs
):
wait_for_input_end.done()
return
if timeout_s is not None and time.monotonic() - start_time > timeout_s:
raise TimeoutError("Timeout waiting for end of input")
wait_for_input_end.check()
time.sleep(3)
def transaction(pipeline: Pipeline, duration_seconds: int):
"""Run a transaction for a specified duration."""
log(f"Running transaction for {duration_seconds} seconds")
pipeline.start_transaction()
time.sleep(duration_seconds)
log("Committing transaction")
commit_start = time.monotonic()
pipeline.commit_transaction()
log(f"Transaction committed in {time.monotonic() - commit_start} seconds")
def transaction_num_records(pipeline: Pipeline, num_records: int):
"""Run a transaction until it ingests a record count or reaches end of input."""
log(f"Running transaction for {num_records} records or end of input")
initial_records = number_of_processed_records(pipeline)
pipeline.start_transaction()
while not check_end_of_input(pipeline):
processed_records = number_of_processed_records(pipeline) - initial_records
if processed_records >= num_records:
break
time.sleep(3)
log("Committing transaction")
commit_start = time.monotonic()
pipeline.commit_transaction()
log(f"Transaction committed in {time.monotonic() - commit_start} seconds")
def checkpoint_pipeline(pipeline: Pipeline):
"""Create a checkpoint and wait for it to complete."""
log("Creating checkpoint")
checkpoint_start = time.monotonic()
pipeline.checkpoint(wait=True)
log(f"Checkpoint complete in {time.monotonic() - checkpoint_start} seconds")
def check_for_endpoint_errors(pipeline: Pipeline):
"""Check for errors on all input and output endpoints."""
for input_endpoint_status in pipeline.stats().inputs:
input_endpoint_status.metrics
if input_endpoint_status.metrics.num_transport_errors > 0:
raise RuntimeError(
f"Transport errors detected on input endpoint: {input_endpoint_status.endpoint_name}"
)
if input_endpoint_status.metrics.num_parse_errors > 0:
raise RuntimeError(
f"Parse errors on input endpoint: {input_endpoint_status.endpoint_name}"
)
log(f" Input endpoint {input_endpoint_status.endpoint_name} OK")
for output_endpoint_status in pipeline.stats().outputs:
output_endpoint_status.metrics
if output_endpoint_status.metrics.num_transport_errors > 0:
raise RuntimeError(
f"Transport errors detected on output endpoint: {output_endpoint_status.endpoint_name}"
)
if output_endpoint_status.metrics.num_encode_errors > 0:
raise RuntimeError(
f"Encode errors on output endpoint: {output_endpoint_status.endpoint_name}"
)
log(f" Output endpoint {output_endpoint_status.endpoint_name} OK")
def number_of_processed_records(pipeline: Pipeline) -> int:
"""Get the total_processed_records metric."""
return pipeline.stats().global_metrics.total_processed_records
def number_of_input_records(pipeline: Pipeline) -> int:
"""Get the total_input_records metric."""
return pipeline.stats().global_metrics.total_input_records
def run_workload(
pipeline_name: str,
tables: dict,
views: List[ViewSpec],
transaction: bool = True,
stop: bool = True,
resources: Optional[Resources] = None,
) -> Pipeline:
"""
Helper to run a pipeline to completion and validate the views afterwards using ad-hoc queries.
Use this for large-scale workload and standard benchmarks (like TPC-H etc.) where you plan to
ingest a lot of data and validate the results. For testing more specific functionality, see
frameworks in the `tests` directory.
"""
pipeline = build_pipeline(pipeline_name, tables, views, resources)
try:
pipeline.start()
start_time = time.monotonic()
if transaction:
try:
pipeline.start_transaction()
except Exception as e:
log(f"Error starting transaction: {e}")
if transaction:
wait_end_of_input(pipeline, timeout_s=3600)
else:
pipeline.wait_for_completion(force_stop=False, timeout_s=3600)
elapsed = time.monotonic() - start_time
log(f"Data ingested in {elapsed}")
if transaction:
start_time = time.monotonic()
try:
pipeline.commit_transaction(
transaction_id=None, wait=True, timeout_s=None
)
log(f"Commit took {time.monotonic() - start_time}")
except Exception as e:
log(f"Error committing transaction: {e}")
log("Waiting for outputs to flush")
start_time = time.monotonic()
pipeline.wait_for_completion(force_stop=False, timeout_s=3600)
log(f"Flushing outputs took {time.monotonic() - start_time}")
validate_outputs(pipeline, tables, views)
finally:
if stop:
try:
pipeline.stop(force=True)
pipeline.clear_storage()
except Exception as e:
log(f"Error during pipeline cleanup: {e}")
return pipeline
You are visitor 01402520 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.