Skip to content

Refactor: extract callCtx struct to eliminate long parameter lists in B2BUA response loops #41

Description

@thorsager

Summary

Five functions in internal/b2bua/handler.go share the same ~20-parameter call signature, making the code fragile and hard to extend:

Function Line Param Count Callers
trunkResponseLoop 830 23 handleTrunkInvite (1 call)
handleTrunk200OK 927 19 trunkResponseLoop (1 call)
b2buaResponseLoop 1162 23 handleInternalInvite (1 call)
handleBob200OK 1318 19 b2buaResponseLoop (1 call)

Every new feature (session timers, PRACK, History-Info, etc.) requires threading yet another parameter through all four functions.

Proposed refactoring

Extract a context struct that holds the shared call state and pass a pointer instead of individual parameters.

Step 1: Define callCtx

type callCtx struct {
    req            *proto.SIPMessage
    tx             sip.Transaction
    target         *sip.Target
    transportImpl  sip.Transport
    uac            *sip.UACTransaction
    rtpConnA       *media.RTPConn
    rtpConnB       *media.RTPConn
    from           *proto.SIPAddress
    aliceFromTag   string
    serverTag      string
    callID         string
    calleeTag      string
    bobCallID      string
    to             *proto.SIPAddress
    selectedPT     uint8
    hasEarlyOffer  bool
    aliceSDPOffer  *proto.SDP
    aliceSDPBytes  []byte
    recordRoute    string
    serverPort     string
}

This collapses the 19 shared parameters into a single struct field.

Step 2: Collapse signatures

Before:

func (h *Handler) trunkResponseLoop(ctx context.Context,
    req, tx, target, transportImpl, uac, rtpConnA, rtpConnB, ... 23 params ...
) {

After:

func (h *Handler) trunkResponseLoop(ctx context.Context, cc *callCtx,
    bobInvite *proto.SIPMessage,
    trunkName string, bobReqURI string,
    sessionExpires time.Duration,
) {

Ditto for the other three functions — they each keep only their specific parameters.

Step 3: Promote serverPort to Handler

serverPort is computed as net.SplitHostPort(h.serverAddr) in handleTrunkInvite, handleInternalInvite, HandleBye, and the session timer. It never changes after startup. Store it as a Handler field (e.g. serverPort string) during NewHandler, removing it from callCtx and from all parameter lists.

type Handler struct {
    // ... existing fields ...
    serverPort string
}

// In NewHandler:
hostPort := net.SplitHostPort(cfg.Server.Addr())
if hostPort == "" { hostPort = "5060" }
h.serverPort = hostPort

Step 4: Trunk-specific extension

For the trunk path, the callCtx can embed or be extended inline:

func (h *Handler) handleTrunkInvite(...) {
    cc := &callCtx{
        req: req, tx: tx, rtpConnA: rtpConnA, // ... etc
    }
    // bobReqURI and sessionExpires are trunk-specific and stay as params
    go h.trunkResponseLoop(ctx, cc, bobInvite, trk.Name, bobReqURI, sessionExpires)
}

Step 5: Visitor pattern (optional future enhancement)

After the struct extraction, the trunk and internal 200 OK handlers (handleTrunk200OK, handleBob200OK) still contain near-identical logic for constructing dialogs, sessions, and bridges from the completed call. A follow-up could extract a method:

func (h *Handler) finalizeCall(ctx context.Context, cc *callCtx, bobTo *proto.SIPAddress, resp *proto.SIPMessage, trunkName string) *Call

Benefits

  1. Modularity — Adding a new field (e.g. History-Info headers) means adding one field to callCtx instead of threading through 4 function signatures
  2. Testability — Callers can construct a callCtx directly in tests
  3. Correctness — Eliminates the risk of passing parameters in the wrong order (two adjacent string parameters are currently order-dependent)
  4. Readability — Each function signature drops from ~5 screenfuls to 2–3 lines

Checklist

  • Add serverPort field to Handler, initialize in NewHandler, remove all net.SplitHostPort recomputations
  • Define callCtx struct with shared fields
  • Refactor b2buaResponseLoop + handleBob200OK to use callCtx
  • Refactor trunkResponseLoop + handleTrunk200OK to use callCtx
  • Update callers in handleInternalInvite and handleTrunkInvite
  • Run make test and make lint

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions