Skip to content

"Only Local" routing silently routes to the realm, and "Hosted by" can never target your own instance #4824

Description

@allister-beamable

When the editor's USAM has lost track of a locally running service - the state described in #4782, where the play button never turns blue while the service is demonstrably alive - the Routing Mode dropdown offers no way out. Selecting "Only Local" does not route locally, and "Hosted by ..." (the one mode that would work) can never target your own instance by construction.

The net effect is that the only unconditional routing path in the system is structurally unavailable to the person who needs it, and the option that looks like the remedy fails silently.

1. "Only Local" is gated exactly like "Automatic", so it silently routes to the realm

client/Packages/com.beamable/Editor/Server/Usam/ServiceRoutingStrategy.cs, UsamRoutingStrategy.GetMap():

case RoutingOptionType.LOCAL:
case RoutingOptionType.AUTO:

    if (_usam.TryGetStatus(setting.beamoId, out status))
    {
        foreach (var route in status.availableRoutes)
        {
            if (route.routingKey == _usam.latestManifest.localRoutingKey && route.instances.Count > 0)
            {
                map[routableServiceName] = route.routingKey;
                break;
            }
        }
    }

    break;
case RoutingOptionType.FRIEND:
    map[routableServiceName] = setting.selectedOption.routingKey;
    break;

LOCAL shares a case with AUTO and therefore shares the instances.Count > 0 gate. When USAM's instance count is stale or zero, no key is emitted - and since an absent X-BEAM-SERVICE-ROUTING-KEY header means remote, the request goes to the realm.

This is worse than the AUTO ambiguity #4782 describes. In AUTO, falling back to remote is at least defensible as a fallback. In LOCAL the user has explicitly said "only local", and gets the realm with nothing logged.

The dropdown also gives no hint this will happen. UsamService.cs adds a LOCAL option even when no live route was found:

if (localOption == null)
{
    setting.options.Add(new RoutingOption
    {
        display = "local",
        type = RoutingOptionType.LOCAL,
        instance = null,
        routingKey = latestManifest.localRoutingKey
    });
}

So "Only Local" is always selectable, is indistinguishable from a working "Only Local", and does nothing.

Minimum fix: honor the user's explicit choice - in LOCAL, emit setting.selectedOption.routingKey unconditionally, as FRIEND already does. If that is considered too blunt, then at minimum the option must be visibly disabled when instance == null, and the silent degradation to remote must be logged.

2. "Hosted by ..." can never be your own instance, by explicit filter

The FRIEND options are built only from routes discovered through Beamo's realm registration list, and your own account is deliberately excluded. cli/cli/Services/DiscoveryService.cs:

var realmRegisteredServicesTask = beamoApi.PostMicroserviceRegistrations(new MicroserviceRegistrationsQuery());
...
foreach (var registration in result.registrations)
{
    // this is only important for remote services. Local services will be caught locally.
    if (registration.startedById == SelfAccountId)
        continue;

Live updates arrive over a websocket subscription to beamo.service_registration_changed. The option list then filters further (UsamService.cs), skipping any route with instances.Count == 0, classifying route.routingKey == localRoutingKey as LOCAL and empty-routing-key as REMOTE, so FRIEND is by elimination "another account's registered service in this realm".

Two consequences:

  • No peer running a service means no FRIEND option at all. Expected, and fine.
  • Your own routing key can never appear as a FRIEND option, because the discovery path drops your own registrations before the editor ever sees them. The filter is keyed on account, not machine or process. So the one code path that sets a routing key unconditionally is unreachable for exactly the case where unconditional routing is what you want.

The ask: a free-text routing-key field

A per-service text field that behaves like FRIEND - set the header unconditionally, no instance check, no discovery requirement - solves both halves:

  • It gives a user whose USAM state is stale a way to reach their own local service without restarting anything.
  • It restores a capability the SDK used to have (see below), and it is the natural home for the "route to a colleague" case too, without depending on discovery having noticed them.

Small change, no protocol implications, and it converts an unrecoverable UI state into a text box. Defect 1 above may be sufficient on its own; this is the more general remedy.

This is not a regression - and here is what actually moved

Worth recording, since "Friend mode presumably worked once" is the natural assumption and the history does not support it.

RoutingOptionType.FRIEND, the instances.Count == 0 filter on the option list, and the shared LOCAL/AUTO gate in GetMap() were all introduced in the same commit: #3717 (c7f52e842, 2024-10-30), when USAM's imgui window landed. The GetMap() body is byte-identical between that commit and today - verified by diff, not by inspection. There has never been a version in which LOCAL was ungated or in which your own instance could be selected as a FRIEND.

What did change is the era before USAM. Manual routing was previously done through MicroserviceIndividualization (UseServicePrefix / GetServicePrefix, PlayerPrefs-backed under BeamableMicroservicePrefixes.{cid}.{pid}), whose last live use was around #2768. It is now [Obsolete] with zero callers anywhere in the client, and IMicroservicePrefixService survives only as a stub whose own comment reads "psuedo obsolete ... this type exists as a backwards compat signal" - both marked at the package-combine, #4093.

So the manual path was not broken by a regression; it was retired when USAM replaced it with a discovery-driven dropdown, and the manual capability was not carried across. That is the gap this issue asks to close. Note too that even the old prefix was never arbitrary text - Prefix => SystemInfo.deviceUniqueIdentifier - so a free-text field would be new capability rather than a restoration in the strict sense.

Why this is worth doing now

#4782 is on file as a local-development-only, pre-existing routing dropout, and was deferred on that basis. That reasoning holds for the dropout itself. It does not account for there being no user-side recovery when it happens.

A customer hit this on 2026-09-03: a locally launched microservice the editor never acknowledged, duplicate instances accumulating because each launch looked like the first, and play-mode calls going to the realm throughout. The same symptom is on record for the same customer in April 2026 on SDK 5.0.0 (play arrow never turning blue, service alive and logging), so it has cost them time twice, five months apart. In both cases the available remedies were process-level - kill orphans, restart the editor - because the routing UI offers nothing that works in that state.

Related

Also noticed while reading, unrelated to the above: UsamRoutingStrategy.GetServiceMap() - the IServiceRoutingStrategy interface method - throws NotImplementedException. The editor path calls GetMap() instead, so nothing is broken today, but any caller reaching for the interface method would fault.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions