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
24 changes: 18 additions & 6 deletions python/feldera/pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -1043,15 +1043,27 @@ def execute(self, query: str):
gen = self.query_tabular(query)
deque(gen, maxlen=0)

def clear_storage(self):
def clear_storage(
self,
wait: bool = True,
timeout_s: float | None = None,
poll_interval_s: float = 0.25,
):
"""
Clears the storage of the pipeline if it is currently in use.
This action cannot be canceled, and will delete all the pipeline
storage.
Clears the storage of the pipeline.
Once started, this action cannot be canceled, and will delete all the pipeline storage.

:param wait: Set `True` to wait for the pipeline storage to become cleared,
or set `False` to immediately return. Default is `True`.
:param timeout_s: Timeout waiting for storage to become cleared.
`None` = no timeout is enforced (default). Not used if `wait=False`.
:param poll_interval_s: Polling interval at which to check while waiting
if storage is cleared (default is every 0.25 seconds). Not used if `wait=False`.
"""

if self.storage_status() == StorageStatus.INUSE:
self.client.clear_storage(self.name)
self.client.clear_storage(
self.name, wait=wait, timeout_s=timeout_s, poll_interval_s=poll_interval_s
)

@property
def name(self) -> str:
Expand Down
33 changes: 23 additions & 10 deletions python/feldera/rest/feldera_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -718,21 +718,33 @@ def dismiss_error_pipeline(
"""
self.http.post(path=f"/pipelines/{pipeline_name}/dismiss_error")

def clear_storage(self, pipeline_name: str, timeout_s: Optional[float] = None):
def clear_storage(
self,
pipeline_name: str,
wait: bool = True,
timeout_s: float | None = None,
poll_interval_s: float = 0.25,
):
"""
Clears the storage from the pipeline.
This operation cannot be canceled.
Clears the storage of the pipeline.
Once started, this action cannot be canceled, and will delete all the pipeline storage.

:param pipeline_name: The name of the pipeline
:param timeout_s: The amount of time in seconds to wait for the storage
to clear.
:param pipeline_name: Name of the pipeline.
:param wait: Set `True` to wait for the pipeline storage to become cleared,
or set `False` to immediately return. Default is `True`.
:param timeout_s: Timeout waiting for storage to become cleared.
`None` = no timeout is enforced (default). Not used if `wait=False`.
:param poll_interval_s: Polling interval at which to check while waiting
if storage is cleared (default is every 0.25 seconds). Not used if `wait=False`.
"""
self.http.post(
path=f"/pipelines/{pipeline_name}/clear",
)

start = time.monotonic()
if not wait:
return

start = time.monotonic()
while True:
if timeout_s is not None and time.monotonic() - start > timeout_s:
raise FelderaTimeoutError(
Expand All @@ -745,11 +757,12 @@ def clear_storage(self, pipeline_name: str, timeout_s: Optional[float] = None):
if status == "Cleared":
return

logging.debug(
"still clearing %s, waiting for 100 more milliseconds",
logger.debug(
"still clearing %s, waiting for %.1f more seconds",
pipeline_name,
poll_interval_s,
)
time.sleep(0.1)
time.sleep(poll_interval_s)

def start_transaction(self, pipeline_name: str) -> int:
"""
Expand Down
88 changes: 88 additions & 0 deletions python/tests/platform/test_pipeline_lifecycle.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from unittest import skip
from feldera.enums import PipelineStatus, ProgramStatus, StorageStatus
from feldera.rest.errors import FelderaAPIError
import time
from http import HTTPStatus
from feldera import PipelineBuilder, Pipeline
Expand Down Expand Up @@ -329,6 +331,92 @@ def test_pipeline_clear(pipeline_name):
)


@gen_pipeline_name
def test_pipeline_clear_using_api(pipeline_name):
"""
Validate storage_status transitions and clear behavior using the Python API.
"""
pipeline = PipelineBuilder(TEST_CLIENT, pipeline_name, "").create_or_replace()

# Initially should be cleared
assert pipeline.storage_status() == StorageStatus.CLEARED

# Clearing should not fail or have an effect while cleared
pipeline.clear_storage()
assert pipeline.storage_status() == StorageStatus.CLEARED

# Starting should make it in-use
pipeline.start()
assert pipeline.storage_status() == StorageStatus.INUSE

# While running, clear is not possible, and it should still be in-use
error_code = None
try:
pipeline.clear_storage()
except FelderaAPIError as e:
error_code = e.error_code
assert error_code == "StorageStatusImmutableUnlessStopped"
assert pipeline.storage_status() == StorageStatus.INUSE

# The same for non-blocking clear
error_code = None
try:
pipeline.clear_storage(wait=False)
except FelderaAPIError as e:
error_code = e.error_code
assert error_code == "StorageStatusImmutableUnlessStopped"
assert pipeline.storage_status() == StorageStatus.INUSE

# After stopping, it should still be in-use
pipeline.stop(force=True)
assert pipeline.storage_status() == StorageStatus.INUSE

# Starting again makes it remain in use
pipeline.start()
assert pipeline.storage_status() == StorageStatus.INUSE
pipeline.stop(force=True)

# Clearing it should work when stopped
assert pipeline.storage_status() == StorageStatus.INUSE
pipeline.clear_storage()
assert pipeline.storage_status() == StorageStatus.CLEARED

# Non-blocking clear should work as well
pipeline.start()
pipeline.stop(force=True)
pipeline.clear_storage(wait=False)
assert pipeline.storage_status() in [StorageStatus.CLEARING, StorageStatus.CLEARED]


@skip # Passing this test requires denying clearing when desired resources status is provisioned.
@gen_pipeline_name
def test_pipeline_clear_while_desired_provisioned(pipeline_name):
"""
This tests the following scenario:
- There is a pipeline that is stopped (`resources_status=Stopped`) and has
state in storage (`storage_status=InUse`).
- The pipeline is started (`resources_desired_status=Provisioned`) without
waiting. It does not transition yet its `resources_status`. In order to make
sure this fact is not based solely on quick timing, the test first started
recompiling the program which takes a few seconds.
- Before it transitions to `Provisioning` the user attempts to clear the pipeline.
This should fail.
"""
pipeline = PipelineBuilder(TEST_CLIENT, pipeline_name, "").create_or_replace()
pipeline.start()
pipeline.stop(force=True)
TEST_CLIENT.patch_pipeline(name=pipeline_name, sql="CREATE TABLE t1 (c1 INT);")
pipeline.start(wait=False)
error_code = None
try:
pipeline.clear_storage(wait=False)
except FelderaAPIError as e:
error_code = e.error_code
assert error_code == "StorageStatusImmutableUnlessStopped", (
f"User was able to clear storage without error or got the wrong error (error={error_code}), which shouldn't happen"
)


@gen_pipeline_name
def test_start_as_standby_fails(pipeline_name):
"""
Expand Down
Loading