mirror of
https://github.com/neondatabase/neon.git
synced 2026-10-27 22:16:46 +00:00
random operations test (#10986)
## Problem We need to test the stability of Neon. ## Summary of changes The test runs random operations on a Neon project. It performs via the Public API calls the following operations: `create a branch`, `delete a branch`, `add a read-only endpoint`, `delete a read-only endpoint`, `restore a branch to a random position in the past`. All the branches and endpoints are loaded with `pgbench`. --------- Co-authored-by: Peter Bendel <peterbendel@neon.tech> Co-authored-by: Alexander Bayandin <alexander@neon.tech>
This commit is contained in:
co-authored by
Peter Bendel
Alexander Bayandin
parent
748539b222
commit
6c2e5c044c
@@ -22,19 +22,62 @@ def connection_parameters_to_env(params: dict[str, str]) -> dict[str, str]:
|
||||
}
|
||||
|
||||
|
||||
# Some API calls not yet implemented.
|
||||
# You may want to copy not-yet-implemented methods from the PR https://github.com/neondatabase/neon/pull/11305
|
||||
class NeonAPI:
|
||||
def __init__(self, neon_api_key: str, neon_api_base_url: str):
|
||||
self.__neon_api_key = neon_api_key
|
||||
self.__neon_api_base_url = neon_api_base_url.strip("/")
|
||||
self.retry_if_possible = False
|
||||
self.attempts = 10
|
||||
self.sleep_before_retry = 1
|
||||
self.retries524 = 0
|
||||
self.retries4xx = 0
|
||||
|
||||
def __request(self, method: str | bytes, endpoint: str, **kwargs: Any) -> requests.Response:
|
||||
if "headers" not in kwargs:
|
||||
kwargs["headers"] = {}
|
||||
kwargs["headers"] = kwargs.get("headers", {})
|
||||
kwargs["headers"]["Authorization"] = f"Bearer {self.__neon_api_key}"
|
||||
|
||||
resp = requests.request(method, f"{self.__neon_api_base_url}{endpoint}", **kwargs)
|
||||
log.debug("%s %s returned a %d: %s", method, endpoint, resp.status_code, resp.text)
|
||||
resp.raise_for_status()
|
||||
for attempt in range(self.attempts):
|
||||
retry = False
|
||||
resp = requests.request(method, f"{self.__neon_api_base_url}{endpoint}", **kwargs)
|
||||
if resp.status_code >= 400:
|
||||
log.error(
|
||||
"%s %s returned a %d: %s",
|
||||
method,
|
||||
endpoint,
|
||||
resp.status_code,
|
||||
resp.text if resp.status_code != 524 else "CloudFlare error page",
|
||||
)
|
||||
else:
|
||||
log.debug("%s %s returned a %d: %s", method, endpoint, resp.status_code, resp.text)
|
||||
if not self.retry_if_possible:
|
||||
resp.raise_for_status()
|
||||
break
|
||||
elif resp.status_code >= 400:
|
||||
if resp.status_code == 422:
|
||||
if resp.json()["message"] == "branch not ready yet":
|
||||
retry = True
|
||||
self.retries4xx += 1
|
||||
elif resp.status_code == 423 and resp.json()["message"] in {
|
||||
"endpoint is in some transitive state, could not suspend",
|
||||
"project already has running conflicting operations, scheduling of new ones is prohibited",
|
||||
}:
|
||||
retry = True
|
||||
self.retries4xx += 1
|
||||
elif resp.status_code == 524:
|
||||
log.info("The request was timed out, trying to get operations")
|
||||
retry = True
|
||||
self.retries524 += 1
|
||||
if retry:
|
||||
log.info("Retrying, attempt %s/%s", attempt + 1, self.attempts)
|
||||
time.sleep(self.sleep_before_retry)
|
||||
continue
|
||||
else:
|
||||
resp.raise_for_status()
|
||||
break
|
||||
else:
|
||||
raise RuntimeError("Max retry count is reached")
|
||||
|
||||
return resp
|
||||
|
||||
@@ -101,6 +144,96 @@ class NeonAPI:
|
||||
|
||||
return cast("dict[str, Any]", resp.json())
|
||||
|
||||
def create_branch(
|
||||
self,
|
||||
project_id: str,
|
||||
branch_name: str | None = None,
|
||||
parent_id: str | None = None,
|
||||
parent_lsn: str | None = None,
|
||||
parent_timestamp: str | None = None,
|
||||
protected: bool | None = None,
|
||||
archived: bool | None = None,
|
||||
init_source: str | None = None,
|
||||
add_endpoint=True,
|
||||
) -> dict[str, Any]:
|
||||
data: dict[str, Any] = {}
|
||||
if add_endpoint:
|
||||
data["endpoints"] = [{"type": "read_write"}]
|
||||
data["branch"] = {}
|
||||
if parent_id:
|
||||
data["branch"]["parent_id"] = parent_id
|
||||
if branch_name:
|
||||
data["branch"]["name"] = branch_name
|
||||
if parent_lsn is not None:
|
||||
data["branch"]["parent_lsn"] = parent_lsn
|
||||
if parent_timestamp is not None:
|
||||
data["branch"]["parent_timestamp"] = parent_timestamp
|
||||
if protected is not None:
|
||||
data["branch"]["protected"] = protected
|
||||
if init_source is not None:
|
||||
data["branch"]["init_source"] = init_source
|
||||
if archived is not None:
|
||||
data["branch"]["archived"] = archived
|
||||
if not data["branch"]:
|
||||
data.pop("branch")
|
||||
resp = self.__request(
|
||||
"POST",
|
||||
f"/projects/{project_id}/branches",
|
||||
headers={
|
||||
"Accept": "application/json",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
json=data,
|
||||
)
|
||||
return cast("dict[str, Any]", resp.json())
|
||||
|
||||
def get_branch_details(self, project_id: str, branch_id: str) -> dict[str, Any]:
|
||||
resp = self.__request(
|
||||
"GET",
|
||||
f"/projects/{project_id}/branches/{branch_id}",
|
||||
headers={
|
||||
"Accept": "application/json",
|
||||
},
|
||||
)
|
||||
return cast("dict[str, Any]", resp.json())
|
||||
|
||||
def delete_branch(self, project_id: str, branch_id: str) -> dict[str, Any]:
|
||||
resp = self.__request(
|
||||
"DELETE",
|
||||
f"/projects/{project_id}/branches/{branch_id}",
|
||||
headers={
|
||||
"Accept": "application/json",
|
||||
},
|
||||
)
|
||||
return cast("dict[str, Any]", resp.json())
|
||||
|
||||
def restore_branch(
|
||||
self,
|
||||
project_id: str,
|
||||
branch_id: str,
|
||||
source_branch_id: str,
|
||||
source_lsn: str | None,
|
||||
source_timestamp: str | None,
|
||||
preserve_under_name: str | None,
|
||||
):
|
||||
data = {"source_branch_id": source_branch_id}
|
||||
if source_lsn:
|
||||
data["source_lsn"] = source_lsn
|
||||
if source_timestamp:
|
||||
data["source_timestamp"] = source_timestamp
|
||||
if preserve_under_name:
|
||||
data["preserve_under_name"] = preserve_under_name
|
||||
log.info("Data: %s", data)
|
||||
resp = self.__request(
|
||||
"POST",
|
||||
f"/projects/{project_id}/branches/{branch_id}/restore",
|
||||
headers={
|
||||
"Accept": "application/json",
|
||||
},
|
||||
json=data,
|
||||
)
|
||||
return cast("dict[str, Any]", resp.json())
|
||||
|
||||
def start_endpoint(
|
||||
self,
|
||||
project_id: str,
|
||||
@@ -176,6 +309,10 @@ class NeonAPI:
|
||||
|
||||
return cast("dict[str, Any]", resp.json())
|
||||
|
||||
def delete_endpoint(self, project_id: str, endpoint_id: str) -> dict[str, Any]:
|
||||
resp = self.__request("DELETE", f"/projects/{project_id}/endpoints/{endpoint_id}")
|
||||
return cast("dict[str,Any]", resp.json())
|
||||
|
||||
def get_connection_uri(
|
||||
self,
|
||||
project_id: str,
|
||||
|
||||
@@ -3185,6 +3185,7 @@ class PgBin:
|
||||
command: list[str],
|
||||
env: Env | None = None,
|
||||
cwd: str | Path | None = None,
|
||||
stderr_pipe: Any | None = None,
|
||||
) -> subprocess.Popen[Any]:
|
||||
"""
|
||||
Run one of the postgres binaries, not waiting for it to finish
|
||||
@@ -3202,7 +3203,9 @@ class PgBin:
|
||||
log.info(f"Running command '{' '.join(command)}'")
|
||||
env = self._build_env(env)
|
||||
self._log_env(env)
|
||||
return subprocess.Popen(command, env=env, cwd=cwd, stdout=subprocess.PIPE, text=True)
|
||||
return subprocess.Popen(
|
||||
command, env=env, cwd=cwd, stdout=subprocess.PIPE, stderr=stderr_pipe, text=True
|
||||
)
|
||||
|
||||
def run(
|
||||
self,
|
||||
|
||||
Reference in New Issue
Block a user