mirror of
https://framagit.org/framasoft/framaspace/argos.git
synced 2025-04-28 09:52:38 +02:00
⚡ — Mutualize check requests (fix #68)
This commit is contained in:
parent
ea23ea7c1f
commit
e0edb50e12
7 changed files with 111 additions and 34 deletions
|
@ -3,6 +3,7 @@
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
- ✨ — IPv4/IPv6 choice for checks, and choice for a dual-stack check (#69)
|
- ✨ — IPv4/IPv6 choice for checks, and choice for a dual-stack check (#69)
|
||||||
|
- ⚡ — Mutualize check requests (#68)
|
||||||
|
|
||||||
## 0.6.1
|
## 0.6.1
|
||||||
|
|
||||||
|
|
|
@ -41,9 +41,10 @@ class ArgosAgent: # pylint: disable-msg=too-many-instance-attributes
|
||||||
self.max_tasks = max_tasks
|
self.max_tasks = max_tasks
|
||||||
self.wait_time = wait_time
|
self.wait_time = wait_time
|
||||||
self.auth = auth
|
self.auth = auth
|
||||||
self._http_client = None
|
self._http_client: httpx.AsyncClient | None = None
|
||||||
self._http_client_v4 = None
|
self._http_client_v4: httpx.AsyncClient | None = None
|
||||||
self._http_client_v6 = None
|
self._http_client_v6: httpx.AsyncClient | None = None
|
||||||
|
self._res_cache: dict[str, httpx.Response] = {}
|
||||||
|
|
||||||
self.agent_id = socket.gethostname()
|
self.agent_id = socket.gethostname()
|
||||||
|
|
||||||
|
@ -51,6 +52,7 @@ class ArgosAgent: # pylint: disable-msg=too-many-instance-attributes
|
||||||
async def run(self):
|
async def run(self):
|
||||||
auth_header = {
|
auth_header = {
|
||||||
"Authorization": f"Bearer {self.auth}",
|
"Authorization": f"Bearer {self.auth}",
|
||||||
|
"User-Agent": f"Argos Panoptes agent {VERSION}",
|
||||||
}
|
}
|
||||||
self._http_client = httpx.AsyncClient(headers=auth_header)
|
self._http_client = httpx.AsyncClient(headers=auth_header)
|
||||||
|
|
||||||
|
@ -74,37 +76,36 @@ class ArgosAgent: # pylint: disable-msg=too-many-instance-attributes
|
||||||
logger.info("Waiting %i seconds before next retry", self.wait_time)
|
logger.info("Waiting %i seconds before next retry", self.wait_time)
|
||||||
await asyncio.sleep(self.wait_time)
|
await asyncio.sleep(self.wait_time)
|
||||||
|
|
||||||
|
async def _do_request(self, group: str, details: dict):
|
||||||
|
try:
|
||||||
|
if details["ip_version"] == "4":
|
||||||
|
response = await self._http_client_v4.request( # type: ignore[union-attr]
|
||||||
|
method=details["method"], url=details["url"], timeout=60
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
response = await self._http_client_v6.request( # type: ignore[union-attr]
|
||||||
|
method=details["method"], url=details["url"], timeout=60
|
||||||
|
)
|
||||||
|
except httpx.ReadError:
|
||||||
|
sleep(1)
|
||||||
|
if details["ip_version"] == "4":
|
||||||
|
response = await self._http_client_v4.request( # type: ignore[union-attr]
|
||||||
|
method=details["method"], url=details["url"], timeout=60
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
response = await self._http_client_v6.request( # type: ignore[union-attr]
|
||||||
|
method=details["method"], url=details["url"], timeout=60
|
||||||
|
)
|
||||||
|
|
||||||
|
self._res_cache[group] = response
|
||||||
|
|
||||||
async def _complete_task(self, _task: dict) -> AgentResult:
|
async def _complete_task(self, _task: dict) -> AgentResult:
|
||||||
try:
|
try:
|
||||||
task = Task(**_task)
|
task = Task(**_task)
|
||||||
|
|
||||||
url = task.url
|
|
||||||
if task.check == "http-to-https":
|
|
||||||
url = str(httpx.URL(task.url).copy_with(scheme="http"))
|
|
||||||
|
|
||||||
try:
|
|
||||||
if task.ip_version == "4":
|
|
||||||
response = await self._http_client_v4.request( # type: ignore[attr-defined]
|
|
||||||
method=task.method, url=url, timeout=60
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
response = await self._http_client_v6.request( # type: ignore[attr-defined]
|
|
||||||
method=task.method, url=url, timeout=60
|
|
||||||
)
|
|
||||||
except httpx.ReadError:
|
|
||||||
sleep(1)
|
|
||||||
if task.ip_version == "4":
|
|
||||||
response = await self._http_client_v4.request( # type: ignore[attr-defined]
|
|
||||||
method=task.method, url=url, timeout=60
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
response = await self._http_client_v6.request( # type: ignore[attr-defined]
|
|
||||||
method=task.method, url=url, timeout=60
|
|
||||||
)
|
|
||||||
|
|
||||||
check_class = get_registered_check(task.check)
|
check_class = get_registered_check(task.check)
|
||||||
check = check_class(task)
|
check = check_class(task)
|
||||||
result = await check.run(response)
|
result = await check.run(self._res_cache[task.task_group])
|
||||||
status = result.status
|
status = result.status
|
||||||
context = result.context
|
context = result.context
|
||||||
|
|
||||||
|
@ -123,10 +124,34 @@ class ArgosAgent: # pylint: disable-msg=too-many-instance-attributes
|
||||||
)
|
)
|
||||||
|
|
||||||
if response.status_code == httpx.codes.OK:
|
if response.status_code == httpx.codes.OK:
|
||||||
# XXX Maybe we want to group the tests by URL ? (to issue one request per URL)
|
|
||||||
data = response.json()
|
data = response.json()
|
||||||
logger.info("Received %i tasks from the server", len(data))
|
logger.info("Received %i tasks from the server", len(data))
|
||||||
|
|
||||||
|
req_groups = {}
|
||||||
|
for _task in data:
|
||||||
|
task = Task(**_task)
|
||||||
|
|
||||||
|
url = task.url
|
||||||
|
group = task.task_group
|
||||||
|
|
||||||
|
if task.check == "http-to-https":
|
||||||
|
url = str(httpx.URL(task.url).copy_with(scheme="http"))
|
||||||
|
group = f"{task.method}-{task.ip_version}-{url}"
|
||||||
|
_task["task_group"] = group
|
||||||
|
|
||||||
|
req_groups[group] = {
|
||||||
|
"url": url,
|
||||||
|
"ip_version": task.ip_version,
|
||||||
|
"method": task.method,
|
||||||
|
}
|
||||||
|
|
||||||
|
requests = []
|
||||||
|
for group, details in req_groups.items():
|
||||||
|
requests.append(self._do_request(group, details))
|
||||||
|
|
||||||
|
if requests:
|
||||||
|
await asyncio.gather(*requests)
|
||||||
|
|
||||||
tasks = []
|
tasks = []
|
||||||
for task in data:
|
for task in data:
|
||||||
tasks.append(self._complete_task(task))
|
tasks.append(self._complete_task(task))
|
||||||
|
|
|
@ -23,6 +23,7 @@ class Task(BaseModel):
|
||||||
check: str
|
check: str
|
||||||
method: Method
|
method: Method
|
||||||
expected: str
|
expected: str
|
||||||
|
task_group: str
|
||||||
selected_at: datetime | None
|
selected_at: datetime | None
|
||||||
selected_by: str | None
|
selected_by: str | None
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
"""Add task index
|
||||||
|
|
||||||
|
Revision ID: 8b58ced14d6e
|
||||||
|
Revises: 64f73a79b7d8
|
||||||
|
Create Date: 2024-12-03 16:41:44.842213
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = "8b58ced14d6e"
|
||||||
|
down_revision: Union[str, None] = "64f73a79b7d8"
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
with op.batch_alter_table("tasks", schema=None) as batch_op:
|
||||||
|
batch_op.add_column(sa.Column("task_group", sa.String(), nullable=True))
|
||||||
|
with op.batch_alter_table("tasks", schema=None) as batch_op:
|
||||||
|
batch_op.execute(
|
||||||
|
"UPDATE tasks SET task_group = method || '-' || ip_version || '-' || url"
|
||||||
|
)
|
||||||
|
batch_op.alter_column("task_group", nullable=False)
|
||||||
|
batch_op.create_index("similar_tasks", ["task_group"], unique=False)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
with op.batch_alter_table("tasks", schema=None) as batch_op:
|
||||||
|
batch_op.drop_index("similar_tasks")
|
||||||
|
batch_op.drop_column("task_group")
|
|
@ -9,12 +9,21 @@ from sqlalchemy import (
|
||||||
ForeignKey,
|
ForeignKey,
|
||||||
)
|
)
|
||||||
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
|
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
|
||||||
|
from sqlalchemy.schema import Index
|
||||||
|
|
||||||
from argos.checks import BaseCheck, get_registered_check
|
from argos.checks import BaseCheck, get_registered_check
|
||||||
from argos.schemas import WebsiteCheck
|
from argos.schemas import WebsiteCheck
|
||||||
from argos.schemas.utils import IPVersion, Method
|
from argos.schemas.utils import IPVersion, Method
|
||||||
|
|
||||||
|
|
||||||
|
def compute_task_group(context) -> str:
|
||||||
|
return (
|
||||||
|
f"{context.current_parameters['method']}-"
|
||||||
|
f"{context.current_parameters['ip_version']}-"
|
||||||
|
f"{context.current_parameters['url']}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Base(DeclarativeBase):
|
class Base(DeclarativeBase):
|
||||||
type_annotation_map = {List[WebsiteCheck]: JSON, dict: JSON}
|
type_annotation_map = {List[WebsiteCheck]: JSON, dict: JSON}
|
||||||
|
|
||||||
|
@ -62,6 +71,7 @@ class Task(Base):
|
||||||
selected_at: Mapped[datetime] = mapped_column(nullable=True)
|
selected_at: Mapped[datetime] = mapped_column(nullable=True)
|
||||||
completed_at: Mapped[datetime] = mapped_column(nullable=True)
|
completed_at: Mapped[datetime] = mapped_column(nullable=True)
|
||||||
next_run: Mapped[datetime] = mapped_column(nullable=True)
|
next_run: Mapped[datetime] = mapped_column(nullable=True)
|
||||||
|
task_group: Mapped[str] = mapped_column(insert_default=compute_task_group)
|
||||||
|
|
||||||
severity: Mapped[Literal["ok", "warning", "critical", "unknown"]] = mapped_column(
|
severity: Mapped[Literal["ok", "warning", "critical", "unknown"]] = mapped_column(
|
||||||
Enum("ok", "warning", "critical", "unknown", name="severity"),
|
Enum("ok", "warning", "critical", "unknown", name="severity"),
|
||||||
|
@ -75,7 +85,7 @@ class Task(Base):
|
||||||
passive_deletes=True,
|
passive_deletes=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self) -> str:
|
||||||
return f"DB Task {self.url} (IPv{self.ip_version}) - {self.check} - {self.expected}"
|
return f"DB Task {self.url} (IPv{self.ip_version}) - {self.check} - {self.expected}"
|
||||||
|
|
||||||
def get_check(self) -> BaseCheck:
|
def get_check(self) -> BaseCheck:
|
||||||
|
@ -117,6 +127,9 @@ class Task(Base):
|
||||||
return self.last_result.status
|
return self.last_result.status
|
||||||
|
|
||||||
|
|
||||||
|
Index("similar_tasks", Task.task_group)
|
||||||
|
|
||||||
|
|
||||||
class Result(Base):
|
class Result(Base):
|
||||||
"""There are multiple results per task.
|
"""There are multiple results per task.
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ from hashlib import sha256
|
||||||
from typing import List
|
from typing import List
|
||||||
from urllib.parse import urljoin
|
from urllib.parse import urljoin
|
||||||
|
|
||||||
from sqlalchemy import asc, desc, func
|
from sqlalchemy import asc, desc, func, Select
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from argos import schemas
|
from argos import schemas
|
||||||
|
@ -14,15 +14,16 @@ from argos.server.models import Result, Task, ConfigCache, User
|
||||||
|
|
||||||
async def list_tasks(db: Session, agent_id: str, limit: int = 100):
|
async def list_tasks(db: Session, agent_id: str, limit: int = 100):
|
||||||
"""List tasks and mark them as selected"""
|
"""List tasks and mark them as selected"""
|
||||||
tasks = (
|
subquery = (
|
||||||
db.query(Task)
|
db.query(func.distinct(Task.task_group))
|
||||||
.filter(
|
.filter(
|
||||||
Task.selected_by == None, # noqa: E711
|
Task.selected_by == None, # noqa: E711
|
||||||
((Task.next_run <= datetime.now()) | (Task.next_run == None)), # noqa: E711
|
((Task.next_run <= datetime.now()) | (Task.next_run == None)), # noqa: E711
|
||||||
)
|
)
|
||||||
.limit(limit)
|
.limit(limit)
|
||||||
.all()
|
.subquery()
|
||||||
)
|
)
|
||||||
|
tasks = db.query(Task).filter(Task.task_group.in_(Select(subquery))).all()
|
||||||
|
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
for task in tasks:
|
for task in tasks:
|
||||||
|
|
|
@ -37,6 +37,7 @@ def ssl_task(now):
|
||||||
domain="https://example.org",
|
domain="https://example.org",
|
||||||
ip_version="6",
|
ip_version="6",
|
||||||
method="GET",
|
method="GET",
|
||||||
|
task_group="GET-6-https://example.org",
|
||||||
check="ssl-certificate-expiration",
|
check="ssl-certificate-expiration",
|
||||||
expected="on-check",
|
expected="on-check",
|
||||||
selected_at=now,
|
selected_at=now,
|
||||||
|
|
Loading…
Reference in a new issue