Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ci(perf): Add workflow concurrency stress tests #209

Draft
wants to merge 34 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
77e7fb3
Add pytest-benchmark workflow
topher-lo Jun 24, 2024
36d2522
test: Parameterize stress test
topher-lo Jun 24, 2024
2bc999f
fix paths
topher-lo Jun 24, 2024
48a8858
Ignore frontend in benchmark runs
topher-lo Jun 24, 2024
9455bfa
fix: posix path stem getter
topher-lo Jul 2, 2024
74be767
fix: drop elastic and temporal ui services
topher-lo Jul 2, 2024
0cbe479
build: Missing pytest-benchmark
topher-lo Jul 2, 2024
02b3419
Disable benchmarks in test
topher-lo Jul 2, 2024
9cf3239
asyncio run
topher-lo Jul 2, 2024
9ea1221
Missing image tag in benchmark
topher-lo Jul 2, 2024
9eaf36c
Use asyncio run
topher-lo Jul 2, 2024
2748b87
test: Remove --scale worker
topher-lo Jul 2, 2024
af279b1
fix: Use main image on push to main
topher-lo Jul 2, 2024
772ed52
ci: Remove build image on main
topher-lo Jul 2, 2024
c87c69a
ci: Update paths
topher-lo Jul 2, 2024
a6afffa
missing pip install tracecat
topher-lo Jul 2, 2024
05984bf
Merge branch 'main' into ci/add-stress-tests
topher-lo Jul 3, 2024
4b956b7
fix: Outdated cli command
topher-lo Jul 3, 2024
14ead74
fix: Change temporal cluster url in pytests to localhost
topher-lo Jul 3, 2024
74fc817
feat: Add temporal health endpoint to tracecat api
topher-lo Jul 3, 2024
ff074b9
verify TC-temporal connection
topher-lo Jul 3, 2024
ff66a78
revert: Don't need temporal url in playbooks test
topher-lo Jul 3, 2024
1a6afa5
Move stress tests into integration folder
topher-lo Jul 3, 2024
ad4d4d3
fix: Stress test via calling tracecat API
topher-lo Jul 3, 2024
8725636
Remove health checks
topher-lo Jul 3, 2024
101a007
fix: dsl
topher-lo Jul 3, 2024
0e6e000
Move dsl fixture into conftest
topher-lo Jul 3, 2024
efa6092
Move all dsl fixtures into conftest
topher-lo Jul 3, 2024
05b873a
fix: path to dsl yaml
topher-lo Jul 3, 2024
8182ba6
remove ids
topher-lo Jul 3, 2024
615f888
Fix path to data
topher-lo Jul 3, 2024
e95f1dc
Only use stem
topher-lo Jul 3, 2024
5ad3abb
Fix cannot call list in asyncio gather
topher-lo Jul 3, 2024
4620cb6
fix: run gather
topher-lo Jul 3, 2024
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
67 changes: 67 additions & 0 deletions .github/workflows/benchmark.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
name: Benchmark

on:
push:
branches: ["main"]
paths:
- "docker-compose.yml"
- "Dockerfile"
- "pyproject.toml"
- "tests/**"
- "tracecat-cli/**"
- "tracecat/**"
pull_request:
branches: ["main"]
paths: [".github/workflows/benchmark.yml"]

jobs:
stress-test:
runs-on: ubuntu-latest-8-cores
timeout-minutes: 30
strategy:
matrix:
num_workers: ["1", "2", "4", "8"]
steps:
- uses: actions/checkout@v4

- name: Set up Python 3.12
uses: actions/setup-python@v3
with:
python-version: "3.12"
cache: "pip"
cache-dependency-path: pyproject.toml

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Run environment setup script
run: bash env.sh

- name: Get image tag
id: set-image-tag
run: |
if [ "${{ github.event_name }}" == "push" ] && [ "${{ github.ref }}" == "refs/heads/main" ]; then
echo "TRACECAT__IMAGE_TAG=main" >> $GITHUB_ENV
else
echo "TRACECAT__IMAGE_TAG=pr-${{ github.event.pull_request.number }}" >> $GITHUB_ENV
fi

- name: Start Docker services
env:
NUM_WORKERS: ${{ matrix.num_workers }}
run: |
docker compose up --no-deps api worker postgres_db temporal -d

- name: pip install Tracecat
run: python -m pip install --upgrade pip && pip install ".[dev]" && pip install ./cli

- name: Run tests (headless mode)
env:
TRACECAT__IMAGE_TAG: ${{ env.TRACECAT__IMAGE_TAG }}
LOG_LEVEL: WARNING
run: |
pytest -k "test_stress" --temporal-no-restart --tracecat-no-restart
--benchmark-name=short \
--benchmark-group-by=param \
--benchmark-warmup=off \
--benchmark-columns=min,max,mean,median,stddev,iterations \
2 changes: 0 additions & 2 deletions .github/workflows/build-push-images.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ name: Publish Images

on:
push:
branches:
- "main"
tags:
- "*.*.*"

Expand Down
30 changes: 15 additions & 15 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,22 @@ name: Tests
on:
push:
branches: ["main"]
paths-ignore:
- "frontend/**"
- "docs/**"
- "README.md"
paths:
- "docker-compose.yml"
- "Dockerfile"
- "pyproject.toml"
- "tests/**"
- "tracecat-cli/**"
- "tracecat/**"
pull_request:
branches: ["main"]
paths-ignore:
- "frontend/**"
- "docs/**"
- "README.md"
paths:
- "docker-compose.yml"
- "Dockerfile"
- "pyproject.toml"
- "tests/**"
- "tracecat-cli/**"
- "tracecat/**"

permissions:
contents: read
Expand Down Expand Up @@ -100,9 +106,6 @@ jobs:
- name: Start Docker services
run: docker compose up --no-deps api worker postgres_db -d

- name: Verify Tracecat API is running
run: curl -s http://localhost:8000/health | jq -e '.status == "ok"'

- name: pip install Tracecat
run: python -m pip install --upgrade pip && pip install ".[dev]" && pip install ./cli

Expand Down Expand Up @@ -154,9 +157,6 @@ jobs:
- name: Start Docker services
run: docker compose up --no-deps api worker postgres_db temporal -d

- name: Verify Tracecat API is running
run: curl -s http://localhost:8000/health | jq -e '.status == "ok"'

- name: pip install Tracecat
run: python -m pip install --upgrade pip && pip install ".[dev]" && pip install ./cli

Expand Down Expand Up @@ -202,4 +202,4 @@ jobs:
URLSCAN_API_KEY: ${{ secrets.INTEGRATION__URLSCAN_API_KEY }}
VT_API_KEY: ${{ secrets.INTEGRATION__VT_API_KEY }}
LOG_LEVEL: WARNING
run: pytest -k "test_playbooks" --temporal-no-restart --tracecat-no-restart
run: pytest -k "test_playbooks" --benchmark-disable --temporal-no-restart --tracecat-no-restart
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ Repository = "https://github.com/TracecatHQ/tracecat"
dev = [
"respx",
"pytest",
"pytest-benchmark",
"python-dotenv",
"pytest-asyncio",
"pytest-mock==3.14.0",
Expand Down
77 changes: 77 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
import asyncio
import os
import subprocess
import time
from pathlib import Path
from uuid import uuid4

import pytest
import yaml
from cryptography.fernet import Fernet
from loguru import logger

from tracecat.dsl.common import DSLInput

DATA_PATH = Path(__file__).parent.joinpath("data/workflows")


def pytest_addoption(parser: pytest.Parser):
parser.addoption(
Expand Down Expand Up @@ -171,3 +178,73 @@ def tracecat_worker(env_sandbox):
["docker", "compose", "down", "--remove-orphans", "worker"], check=True
)
logger.info("Stopped Tracecat Temporal worker")


@pytest.fixture
def mock_registry():
"""Mock registry for testing UDFs.

Note
----
- This fixture is used to test the integration of UDFs with the workflow.
- It's unreachable by an external worker, as the worker will not have access
to these functions when it starts up.
"""
from tracecat.registry import registry

# NOTE!!!!!!!: Didn't want to spend too much time figuring out how
# to grab the actual execution order using the client, so I'm using a
# hacky way to get the order of execution. TO FIX LATER
# The counter doesn't get reset properly so you should never use this outside
# of the 'ordering' tests
def counter():
i = 0
while True:
yield i
i += 1

counter_gen = counter()
if "integration_test.count" not in registry:

@registry.register(
description="Counts up from 0",
namespace="integration_test",
)
def count(arg: str | None = None) -> int:
order = next(counter_gen)
return order

if "integration_test.passthrough" not in registry:

@registry.register(
description="passes through",
namespace="integration_test",
)
async def passthrough(num: int) -> int:
await asyncio.sleep(0.1)
return num

registry.init()
yield registry
counter_gen = counter() # Reset the counter generator


@pytest.fixture
def dsl(request: pytest.FixtureRequest) -> DSLInput:
test_name = request.param
data_path = DATA_PATH / f"{test_name}.yml"
dsl = DSLInput.from_yaml(data_path)
return dsl


@pytest.fixture
def dsl_with_expected(request: pytest.FixtureRequest) -> DSLInput:
test_name = request.param
data_path = DATA_PATH / f"{test_name}.yml"
expected_path = DATA_PATH / f"{test_name}_expected.yml"
dsl = DSLInput.from_yaml(data_path)
with expected_path.open() as f:
yaml_data = f.read()
data = yaml.safe_load(yaml_data)
expected_result = {key: (value or {}) for key, value in data.items()}
return dsl, expected_result
37 changes: 37 additions & 0 deletions tests/integration/test_stress.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import asyncio
import uuid
from pathlib import Path

import pytest

from tracecat.dsl.dispatcher import dispatch_workflow

DATA_PATH = Path(__file__).parent.parent.joinpath("data/workflows")
TEST_WF_ID = "wf-00000000000000000000000000000000"


@pytest.mark.parametrize(
"dsl",
["stress_adder_tree"],
indirect=True,
)
@pytest.mark.parametrize(
"num_workflows", [10, 100, 1000], ids=lambda x: f"num_workflows={x}"
)
@pytest.mark.slow
def test_concurrent_workflows(
dsl, num_workflows, temporal_cluster, mock_registry, auth_sandbox, benchmark
):
"""Multiple executions of the same workflow run at the same time."""

def generate_wf_id():
return f"wf-{uuid.uuid4()}"

async def run_workflows():
tasks = [
dispatch_workflow(dsl=dsl, wf_id=generate_wf_id())
for _ in range(num_workflows)
]
return await asyncio.gather(*tasks)

benchmark.pedantic(lambda: asyncio.run(run_workflows()), iterations=3, rounds=1)
1 change: 0 additions & 1 deletion tests/integration/test_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ def filename(request: pytest.FixtureRequest) -> Path:
)
@pytest.mark.asyncio
async def test_workflow_commit(filename, auth_sandbox):
print(filename)
title = f"Test workflow: {filename}"
workflow_result = await shared.create_workflow(title)
await shared.commit_workflow(filename, workflow_result["id"])
Loading
Loading