-
-
Notifications
You must be signed in to change notification settings - Fork 4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(hc): Introduce CI job to check for RPC backwards compatibility (#…
…68337) The motivation for the check is that breaking the backwards compatibility of an RPC interface can cause errors on production, even if all call sites are updated in the same code change, because the change is rolled out to the control and region silos asynchronously. Introduce a CI check that will warn the developer if they make such a breaking change. Introduce a `sentry rpcschema` command that prints an approximate schema, in OpenAPI format, of the RPC services to stdout. --------- Co-authored-by: Mark Story <mark@mark-story.com>
- Loading branch information
1 parent
0b85460
commit f18e50d
Showing
8 changed files
with
231 additions
and
10 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
import sys | ||
import traceback | ||
from collections.abc import Iterable | ||
from dataclasses import dataclass | ||
from typing import Any | ||
|
||
import click | ||
from django.urls import reverse | ||
from openapi_pydantic import OpenAPI | ||
from openapi_pydantic.util import PydanticSchema, construct_open_api_with_schema_class | ||
|
||
from sentry.runner.decorators import configuration | ||
from sentry.utils import json | ||
|
||
|
||
@click.command("rpcschema") | ||
@click.option( | ||
"--partial", | ||
is_flag=True, | ||
default=False, | ||
help="Ignore RPC methods that produce errors.", | ||
) | ||
@click.option( | ||
"--diagnose", | ||
is_flag=True, | ||
default=False, | ||
help="List RPC methods that produce errors and suppress all other output.", | ||
) | ||
@configuration | ||
def rpcschema(diagnose: bool, partial: bool) -> None: | ||
from sentry.services.hybrid_cloud.rpc import ( | ||
RpcMethodSignature, | ||
list_all_service_method_signatures, | ||
) | ||
|
||
@dataclass | ||
class RpcSchemaEntry: | ||
sig: RpcMethodSignature | ||
|
||
@property | ||
def api_path(self) -> str: | ||
return reverse( | ||
"sentry-api-0-rpc-service", args=(self.sig.service_key, self.sig.method_name) | ||
) | ||
|
||
def build_api_entry(self) -> dict[str, Any]: | ||
param_schema, return_schema = self.sig.get_schemas() | ||
return { | ||
"post": { | ||
"description": "Execute an RPC", | ||
"requestBody": { | ||
"content": { | ||
"application/json": { | ||
"schema": PydanticSchema(schema_class=param_schema) | ||
} | ||
}, | ||
}, | ||
"responses": { | ||
"200": { | ||
"description": "Success", | ||
"content": { | ||
"application/json": { | ||
"schema": PydanticSchema(schema_class=return_schema) | ||
} | ||
}, | ||
} | ||
}, | ||
} | ||
} | ||
|
||
def create_spec(signatures: Iterable[RpcMethodSignature]) -> dict[str, Any]: | ||
entries = [RpcSchemaEntry(sig) for sig in signatures] | ||
path_dict = {entry.api_path: entry.build_api_entry() for entry in entries} | ||
|
||
spec = OpenAPI.parse_obj( | ||
dict( | ||
info=dict( | ||
title="Sentry Internal RPC APIs", | ||
version="0.0.1", | ||
), | ||
servers=[dict(url="https://sentry.io/")], # TODO: Generify with setting value | ||
paths=path_dict, | ||
) | ||
) | ||
spec = construct_open_api_with_schema_class(spec) | ||
return spec.dict(by_alias=True, exclude_none=True) | ||
|
||
def create_partial_spec( | ||
signatures: Iterable[RpcMethodSignature], | ||
) -> tuple[dict[str, Any], list[str]]: | ||
stable_signatures: list[RpcMethodSignature] = [] | ||
error_reports: list[str] = [] | ||
for sig in signatures: | ||
try: | ||
create_spec([sig]) | ||
except Exception as e: | ||
last_line = str(e).split("\n")[-1].strip() | ||
error_reports.append(f"{sig!s}: {last_line}") | ||
if not diagnose: | ||
traceback.print_exc() | ||
else: | ||
stable_signatures.append(sig) | ||
|
||
return create_spec(stable_signatures), error_reports | ||
|
||
all_signatures = list_all_service_method_signatures() | ||
|
||
if diagnose or partial: | ||
spec, error_reports = create_partial_spec(all_signatures) | ||
if diagnose: | ||
print(f"Error count: {len(error_reports)}") # noqa | ||
for bad_sig in error_reports: | ||
print("- " + bad_sig) # noqa | ||
else: | ||
spec = create_spec(all_signatures) | ||
|
||
if not diagnose: | ||
json.dump(spec, sys.stdout) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
import pydantic | ||
|
||
from sentry.services.hybrid_cloud.sig import SerializableFunctionSignature | ||
from sentry.testutils.cases import TestCase | ||
|
||
|
||
class SerializableFunctionSignatureTest(TestCase): | ||
def test_signature(self): | ||
class AnObject(pydantic.BaseModel): | ||
a: int | ||
b: str | ||
|
||
def a_function(arg1: AnObject, arg2: AnObject) -> AnObject: | ||
return AnObject(a=arg1.a + arg2.a, b=".".join((arg1.b, arg2.b))) | ||
|
||
sig = SerializableFunctionSignature(a_function) | ||
arg_values = dict(arg1=AnObject(a=1, b="foo"), arg2=AnObject(a=2, b="bar")) | ||
serialized_arguments = sig.serialize_arguments(arg_values) | ||
assert serialized_arguments == {"arg1": {"a": 1, "b": "foo"}, "arg2": {"a": 2, "b": "bar"}} | ||
|
||
deserialized_arguments = sig.deserialize_arguments(serialized_arguments) | ||
assert isinstance(deserialized_arguments, pydantic.BaseModel) | ||
assert set(deserialized_arguments.__dict__.keys()) == {"arg1", "arg2"} | ||
assert hasattr(deserialized_arguments, "arg1") | ||
assert deserialized_arguments.arg1 == AnObject(a=1, b="foo") | ||
assert hasattr(deserialized_arguments, "arg2") | ||
assert deserialized_arguments.arg2 == AnObject(a=2, b="bar") | ||
|
||
deserialized_return_value = sig.deserialize_return_value(dict(a=3, b="qux")) | ||
assert deserialized_return_value == AnObject(a=3, b="qux") |