Skip to content

Commit 7fc733c

Browse files
authored
Hook up peanutbutter as an LPQ backend (#69187)
Peanutbutter is a self contained service managing per-project budgets. It should eventually replace the existing `realtime_metrics` code which uses redis and celery in an extremely inefficient way. --- This replaces #67232, now using HTTP.
1 parent 70c361f commit 7fc733c

File tree

5 files changed

+118
-0
lines changed

5 files changed

+118
-0
lines changed

.github/actions/setup-sentry/action.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ inputs:
3535
description: 'Is symbolicator required?'
3636
required: false
3737
default: 'false'
38+
peanutbutter:
39+
description: 'Is peanutbutter required?'
40+
required: false
41+
default: 'false'
3842
python-version:
3943
description: 'python version to install'
4044
required: false
@@ -146,6 +150,7 @@ runs:
146150
NEED_CHARTCUTERIE: ${{ inputs.chartcuterie }}
147151
NEED_REDIS_CLUSTER: ${{ inputs.redis_cluster }}
148152
NEED_SYMBOLICATOR: ${{ inputs.symbolicator }}
153+
NEED_PEANUTBUTTER: ${{ inputs.peanutbutter }}
149154
WORKDIR: ${{ inputs.workdir }}
150155
PG_VERSION: ${{ inputs.pg-version }}
151156
ENABLE_AUTORUN_MIGRATION_SEARCH_ISSUES: '1'
@@ -180,6 +185,10 @@ runs:
180185
services+=(symbolicator)
181186
fi
182187
188+
if [ "$NEED_PEANUTBUTTER" = "true" ]; then
189+
services+=(peanutbutter)
190+
fi
191+
183192
if [ "$NEED_KAFKA" = "true" ]; then
184193
services+=(kafka)
185194
fi

.github/workflows/backend.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ jobs:
9999
kafka: true
100100
snuba: true
101101
symbolicator: true
102+
peanutbutter: true
102103
# Right now, we run so few bigtable related tests that the
103104
# overhead of running bigtable in all backend tests
104105
# is way smaller than the time it would take to run in its own job.

src/sentry/conf/server.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2743,6 +2743,11 @@ def custom_parameter_sort(parameter: dict) -> tuple[str, int]:
27432743
# This flag activates Spotlight Sidecar in the development environment
27442744
SENTRY_USE_SPOTLIGHT = False
27452745

2746+
# This flags enables the `peanutbutter` realtime metrics backend.
2747+
# See https://github.com/getsentry/peanutbutter.
2748+
# We do not want/need this in normal devservices, but we need it for certain tests.
2749+
SENTRY_USE_PEANUTBUTTER = False
2750+
27462751
# SENTRY_DEVSERVICES = {
27472752
# "service-name": lambda settings, options: (
27482753
# {
@@ -3002,6 +3007,14 @@ def custom_parameter_sort(parameter: dict) -> tuple[str, int]:
30023007
"only_if": settings.SENTRY_USE_SPOTLIGHT,
30033008
}
30043009
),
3010+
"peanutbutter": lambda settings, options: (
3011+
{
3012+
"image": "us.gcr.io/sentryio/peanutbutter:latest",
3013+
"environment": {},
3014+
"ports": {"4433/tcp": 4433},
3015+
"only_if": settings.SENTRY_USE_PEANUTBUTTER,
3016+
}
3017+
),
30053018
}
30063019

30073020
# Max file size for serialized file uploads in API
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import logging
2+
from collections.abc import Iterable
3+
from urllib.parse import urljoin
4+
5+
from requests import RequestException
6+
7+
from sentry.net.http import Session
8+
9+
from . import base
10+
11+
logger = logging.getLogger(__name__)
12+
13+
# The timeout for rpc calls, in seconds.
14+
# We expect these to be very quick, and never want to block more than 2 ms (4 with connect + read).
15+
RPC_TIMEOUT = 2 / 1000 # timeout in seconds
16+
17+
18+
class PbRealtimeMetricsStore(base.RealtimeMetricsStore):
19+
def __init__(self, target: str):
20+
self.target = target
21+
self.session = Session()
22+
23+
def record_project_duration(self, project_id: int, duration: float) -> None:
24+
url = urljoin(self.target, "/record_spending")
25+
request = {
26+
"config_name": "symbolication-native",
27+
"project_id": project_id,
28+
"spent": duration,
29+
}
30+
try:
31+
self.session.post(
32+
url,
33+
timeout=RPC_TIMEOUT,
34+
json=request,
35+
)
36+
except RequestException:
37+
pass
38+
39+
def is_lpq_project(self, project_id: int) -> bool:
40+
url = urljoin(self.target, "/exceeds_budget")
41+
request = {
42+
"config_name": "symbolication-native",
43+
"project_id": project_id,
44+
}
45+
try:
46+
response = self.session.post(
47+
url,
48+
timeout=RPC_TIMEOUT,
49+
json=request,
50+
)
51+
return response.json()["exceeds_budget"]
52+
except RequestException:
53+
return False
54+
55+
# NOTE: The functions below are just default impls copy-pasted from `DummyRealtimeMetricsStore`.
56+
# They are not used in the actual implementation of recording budget spend,
57+
# and checking if a project is within its budget.
58+
59+
def validate(self) -> None:
60+
pass
61+
62+
def projects(self) -> Iterable[int]:
63+
yield from ()
64+
65+
def get_used_budget_for_project(self, project_id: int) -> float:
66+
return 0.0
67+
68+
def get_lpq_projects(self) -> set[int]:
69+
return set()
70+
71+
def add_project_to_lpq(self, project_id: int) -> bool:
72+
return False
73+
74+
def remove_projects_from_lpq(self, project_ids: set[int]) -> int:
75+
return 0
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from math import floor
2+
from random import random
3+
4+
from sentry.processing.realtime_metrics.pb import PbRealtimeMetricsStore
5+
6+
7+
def test_invalid_target():
8+
# there is no grpc service at that addr
9+
store = PbRealtimeMetricsStore(target="http://localhost:12345")
10+
store.record_project_duration(1, 123456789)
11+
assert not store.is_lpq_project(1)
12+
13+
14+
def test_pb_works():
15+
store = PbRealtimeMetricsStore(target="http://localhost:4433")
16+
17+
project_id = floor(random() * (1 << 32))
18+
assert not store.is_lpq_project(project_id)
19+
store.record_project_duration(project_id, 123456789)
20+
assert store.is_lpq_project(project_id)

0 commit comments

Comments
 (0)