Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Overview
We propose a runtime-focused refactor of the CallMessageProcessor in the Signal Android codebase. Our primary goal is to improve runtime performance, observability, and maintainability by addressing architectural bottlenecks that degrade call message processing under load or in failure-prone scenarios.
This change restructures the monolithic processor into a modular handler-based system, resulting in reduced control flow complexity, clearer data validation, and improved runtime fault isolation. This benefits production reliability and makes it easier to reason about runtime behavior in critical call flows (offer/answer negotiation, ICE updates, hangups).
Runtime Issues Identified
Monolithic Control Flow Hurts Runtime Responsiveness
CallMessageProcessor currently checks all message types in a nested control block (e.g., if trees and method branching), increasing latency and cognitive complexity.
This approach leads to bloated runtime logic that is harder to optimize, profile, or split across teams.
Tight Coupling to Static Dependencies Hinders Flexibility
Direct usage of AppDependencies.signalCallManager and protocolStore prevents modular testing and runtime swapping (e.g., during instrumentation or experimentation).
No Centralized Validation or Fallback Mechanism
When messages contain incomplete or malformed data (e.g., missing opaque blobs or sender IDs), the system logs a warning and returns early.
This leads to silent partial failures, which are difficult to detect and correlate during live issues or postmortems.
Duplicated Runtime Logic Across Handlers
Remote peer identity resolution and logging logic are repeated across message types, increasing the surface area for bugs and inconsistent failure handling.
Runtime Benefits of Proposed Change
Faster and Safer Message Dispatching
Switching to a handler chain model allows fast message routing via canHandle() checks.
Reduces branching and improves runtime traceability by isolating logic per handler such as OfferCallHandler or HangupCallHandler.
Improved Observability and Testability
Handlers are independently testable and mockable, making it easier to simulate and validate runtime conditions.
Validation logic can be centralized or standardized across handlers, improving consistency.
Dependency Injection Unlocks Runtime Substitution
Instead of hard dependencies, SignalCallManager and ProtocolStore are injected into handlers, enabling dynamic replacements for experiments or fallback modes.
Clear Fault Boundaries and Logging
Each handler can include standardized logging and runtime metrics such as per type processing latency, or validation failures.
Makes it easier to instrument message processing and track runtime regressions.
Refactor Summary
Core Interface
interface CallHandler {
fun canHandle(callMessage: CallMessage): Boolean
fun handle(
senderRecipient: Recipient,
envelope: Envelope,
content: Content,
metadata: EnvelopeMetadata,
serverDeliveredTimestamp: Long
)
}
New Processor (Lightweight, Extensible)
class CallMessageProcessor(private val handlers: List) {
fun process(...) {
val callMessage = content.callMessage ?: return
handlers.firstOrNull { it.canHandle(callMessage) }
?.handle(...) ?: warn("No handler for call message type")
}
}
Handlers Are Runtime Isolated Units
class OfferCallHandler(
private val signalCallManager: SignalCallManager,
private val protocolStore: ProtocolStore
) : CallHandler {
override fun canHandle(callMessage: CallMessage) = callMessage.offer != null
override fun handle(...) {
// Runtime-safe logic with validation and injected deps
}
}
Example Registration (Supports Runtime Flexibility)
val callMessageProcessor = CallMessageProcessor(
listOf(
OfferCallHandler(signalCallManager, protocolStore),
AnswerCallHandler(signalCallManager, protocolStore),
IceUpdateCallHandler(signalCallManager),
HangupCallHandler(signalCallManager),
BusyCallHandler(signalCallManager),
OpaqueCallHandler(signalCallManager)
)
)
Future Runtime Extensions Enabled
Runtime configuration to enable or disable specific handlers.
Pluggable instrumentation (timing, logging, validation metrics).
Cleaner CI performance benchmarks per handler type.
More robust error tracking via consistent fallback and logging structure.
Thanks for your time and the great work on Signal
Griffin Urban, Seth Clover, Kasson Plummer, Nathan Voung, Lawson Port