[HINT] Download the complete skill directory including SKILL.md and all related files
name
hybrid-cloud-rpc
description
Guide for creating, updating, and deprecating hybrid cloud RPC services in Sentry. Use when asked to "add RPC method", "create RPC service", "hybrid cloud service", "new RPC model", "deprecate RPC method", "remove RPC endpoint", "cross-silo service", "cell RPC", or "control silo service". Covers service scaffolding, method signatures, RPC models, cell resolvers, testing, and safe deprecation workflows.
Hybrid Cloud RPC Services
This skill guides you through creating, modifying, and deprecating RPC services in Sentry's hybrid cloud architecture. RPC services enable cross-silo communication between the Control silo (user auth, billing, org management) and Cell silos (project data, events, issues).
Critical Constraints
NEVER use from __future__ import annotations in service.py or model.py files.
The RPC framework reflects on type annotations at import time. Forward references break serialization silently.
ALL RPC method parameters must be keyword-only (use * in the signature).
ALL parameters and return types must have full type annotations — no string forward references.
ONLY serializable types are allowed: int, str, bool, float, None, Optional[T], list[T], dict[str, T], RpcModel subclasses, Enum subclasses, datetime.datetime.
The service MUST live in one of the 12 registered discovery packages (see Step 3).
Use Field(repr=False) on sensitive fields (tokens, secrets, keys, config blobs,
metadata dicts) to prevent them from leaking into logs and error reports.
See references/rpc-models.md for the full guide.
Step 1: Determine Operation
Classify what the developer needs:
Intent
Go to
Create a brand-new RPC service
Step 2, then Step 3
Add a method to an existing service
Step 2, then Step 4
Update an existing method's signature
Step 5
Deprecate or remove a method/service
Step 6
Step 2: Determine Silo Mode
The service's local_mode determines where the database-backed implementation runs:
Data lives in...
local_mode
Decorator on methods
Example
Cell silo (projects, events, issues, org data)
SiloMode.CELL
@cell_rpc_method(resolve=...)
OrganizationService
Control silo (users, auth, billing, org mappings)
SiloMode.CONTROL
@rpc_method
OrganizationMemberMappingService
Decision rule: If the Django models you need to query live in the cell database, use SiloMode.CELL. If they live in the control database, use SiloMode.CONTROL.
Cell-silo services require a CellResolutionStrategy on every RPC method so the framework knows which cell to route remote calls to. Load references/resolvers.md for the full resolver table.
Step 3: Create a New Service
Load references/service-template.md for copy-paste file templates.
Directory structure
src/sentry/{domain}/services/{service_name}/
├── __init__.py # Re-exports model and service
├── model.py # RpcModel subclasses (NO future annotations)
├── serial.py # ORM → RpcModel conversion functions
├── service.py # Abstract service class (NO future annotations)
└── impl.py # DatabaseBacked implementation
Registration
The service package MUST be a sub-package of one of these 12 registered discovery packages:
If your service doesn't fit any of these, add a new entry to the service_packages tuple in src/sentry/hybridcloud/rpc/service.py:list_all_service_method_signatures().
Checklist for new services
key is unique across all services (check existing keys with grep -r 'key = "' src/sentry/*/services/*/service.py)
local_mode matches where the data lives
get_local_implementation() returns the DatabaseBacked subclass
Module-level my_service = MyService.create_delegation() at bottom of service.py
__init__.py re-exports models and service
No from __future__ import annotations in service.py or model.py
Step 4: Add or Update Methods
For CELL silo services
Load references/resolvers.md for resolver details.
All errors an RPC method propagates must be done via the return type. Errors are
rewrapped and returned as generic Invalid service request to external callers.
Every RPC service needs three categories of tests: silo mode compatibility, data accuracy, and error handling. Use TransactionTestCase (not TestCase) when tests need outbox processing or on_commit hooks.
7.1 Silo mode compatibility with @all_silo_test
Every service test class MUST use @all_silo_test so tests run in all three modes (MONOLITH, CELL, CONTROL). This ensures the delegation layer works for both local and remote dispatch paths.
from sentry.testutils.cases import TestCase, TransactionTestCase
from sentry.testutils.silo import all_silo_test, assume_test_silo_mode, create_test_cells
@all_silo_testclassMyServiceTest(TestCase):
deftest_get_by_id(self):
org = self.create_organization()
result = my_service.get_by_id(organization_id=org.id, id=thing.id)
assert result isnotNone
For tests that need named cells (e.g., testing cell resolution):
Use assume_test_silo_mode or assume_test_silo_mode_of to switch modes within a test when accessing ORM models that live in a different silo:
deftest_cross_silo_behavior(self):
with assume_test_silo_mode(SiloMode.CELL):
org = self.create_organization()
result = my_service.get_by_id(organization_id=org.id, id=thing.id)
assert result isnotNone
7.2 Serialization round-trip with dispatch_to_local_service
Test that arguments and return values survive serialization/deserialization:
from sentry.hybridcloud.rpc.service import dispatch_to_local_service
deftest_serialization_round_trip(self):
result = dispatch_to_local_service(
"my_service_key",
"my_method",
{"organization_id": org.id, "name": "test"},
)
assert result["value"] isnotNone
7.3 RPC model data accuracy
Validate that RPC models faithfully represent the ORM data. Compare every field of the RPC model against the source ORM object:
Use HybridCloudTestMixin for common cross-silo assertions:
from sentry.testutils.hybrid_cloud import HybridCloudTestMixin
classMyServiceTest(HybridCloudTestMixin, TransactionTestCase):
deftest_member_mapping_synced(self):
self.assert_org_member_mapping(org_member=org_member)
7.5 Error handling
Test that the service handles errors correctly in all silo modes:
deftest_not_found_returns_none(self):
result = my_service.get_by_id(organization_id=org.id, id=99999)
assert result isNonedeftest_missing_org_returns_none(self):
# For methods with return_none_if_mapping_not_found=True
result = my_service.get_by_id(organization_id=99999, id=1)
assert result isNone
Test disabled methods:
from sentry.hybridcloud.rpc.service import RpcDisabledException
from sentry.testutils.helpers.options import override_options
deftest_disabled_method_raises(self):
with override_options({"hybrid_cloud.rpc.disabled-service-methods": ["MyService.my_method"]}):
with pytest.raises(RpcDisabledException):
dispatch_remote_call(None, "my_service_key", "my_method", {"id": 1})
Test that remote exceptions are properly wrapped:
from sentry.hybridcloud.rpc.service import RpcRemoteException
deftest_remote_error_wrapping(self):
if SiloMode.get_current_mode() == SiloMode.CELL:
with pytest.raises(RpcRemoteException):
my_control_service.do_thing_that_fails(...)
Test that failed operations produce no side effects:
deftest_no_side_effects_on_failure(self):
result = my_service.create_conflicting_thing(organization_id=org.id)
assertnot result
with assume_test_silo_mode(SiloMode.CELL):
assertnot MyModel.objects.filter(organization_id=org.id).exists()
Test that any calling code (both direct and indirect) is also appropriately
tested with the correct silo decorators.
7.6 Key imports for testing
from sentry.testutils.cases import TestCase, TransactionTestCase
from sentry.testutils.silo import (
all_silo_test,
control_silo_test,
cell_silo_test,
assume_test_silo_mode,
assume_test_silo_mode_of,
create_test_cells,
)
from sentry.testutils.outbox import outbox_runner
from sentry.testutils.hybrid_cloud import HybridCloudTestMixin
from sentry.hybridcloud.rpc.service import (
dispatch_to_local_service,
dispatch_remote_call,
RpcDisabledException,
RpcRemoteException,
)
Step 8: Verify (Pre-flight Checklist)
Before submitting your PR, verify:
No from __future__ import annotations in service.py or model.py
All RPC method parameters are keyword-only (* separator)
All parameters have explicit type annotations
All types are serializable (primitives, RpcModel, list, Optional, dict, Enum, datetime)
Cell service methods have @cell_rpc_method with appropriate resolver
Control service methods have @rpc_method
@cell_rpc_method / @rpc_method comes BEFORE @abstractmethod
create_delegation() is called at module level at the bottom of service.py
Service package is under one of the 12 registered discovery packages
impl.py implements every abstract method with matching parameter names
serial.py correctly converts ORM models to RPC models
Sensitive fields use Field(repr=False) (tokens, secrets, config, metadata)
Tests use @all_silo_test for full silo mode coverage
Tests validate RPC model field accuracy against ORM objects
Tests verify cross-silo resources (mappings, replicas) are created with correct data