Skip to content

Observatory: add reliable one-shot outbound probe batches and result notifications#6492

Closed
eliotcougar wants to merge 15 commits into
XTLS:mainfrom
eliotcougar:feature/one-shot-observatory-probing
Closed

Observatory: add reliable one-shot outbound probe batches and result notifications#6492
eliotcougar wants to merge 15 commits into
XTLS:mainfrom
eliotcougar:feature/one-shot-observatory-probing

Conversation

@eliotcougar

Copy link
Copy Markdown

Summary

This PR adds an embedder-oriented Observatory API for running a finite, bounded batch of outbound probes through one already-created Xray instance.

It is intended for applications that need to measure many outbounds on demand but cannot afford to create one Xray process or core instance per outbound. A caller can provide the outbound tags, concurrency limit, and sample count, receive progressive result notifications, and retain the measurements completed by the current batch even if it is cancelled or interrupted.

The PR also adds Observatory result-update notifications and probe-deadline reporting. Besides supporting responsive one-shot probing, these APIs provide the coordination required by downstream cached-route warm-start implementations.

Motivation

Applications commonly need to display the delay of many configured outbounds or evaluate policy-group candidates in one user-initiated operation.

Creating an independent Xray process for every outbound can provide isolation, but it is often impractical:

  • Process and core startup are expensive.
  • Each instance duplicates configuration, routing, DNS, transport, and runtime state.
  • Memory and CPU usage grow with the number of outbounds.
  • Mobile operating systems may limit or kill a large group of temporary processes.
  • Startup and IPC overhead can dominate short delay measurements.
  • Running multiple temporary cores in one process is undesirable and can corrupt the main core.

This feature provides a middle ground: one disposable Xray instance can contain all required outbounds and probe them concurrently with bounded resource usage. Embedders can therefore perform reliable bulk probing without requiring one process per outbound and without running multiple Xray cores in the same process.

Public API

The following optional extension interfaces are added:

ObservatoryBatchProbe

type ObservatoryBatchProbe interface {
    Observatory

    ProbeOutboundsDeadline(
        tags []string,
        maxConcurrency int,
        samples int,
    ) (time.Duration, error)

    ProbeOutbounds(
        ctx context.Context,
        tags []string,
        maxConcurrency int,
        samples int,
    ) error
}

ProbeOutbounds runs a finite batch through the outbounds already registered in the current Xray instance.

The caller controls:

  • Which outbound tags are tested.
  • The maximum number of concurrent probe workers.
  • The number of samples collected for each outbound.
  • Cancellation through context.Context.

ProbeOutboundsDeadline reports the configured worst-case probe budget for the same parameters. This allows an embedding application to add only its own process-management grace period instead of relying on an arbitrary fixed timeout.

ObservatoryUpdateNotifier

type ObservatoryUpdateNotifier interface {
    SubscribeObservationUpdates() (
        updates <-chan struct{},
        unsubscribe func(),
    )
}

The notifier publishes a coalesced, non-blocking signal whenever observable results change.

The signal is deliberately not the result itself and is not a routing decision. After receiving it, a consumer queries GetObservation or reevaluates its balancer. This keeps delivery lightweight, prevents slow subscribers from blocking probe workers, and avoids exposing mutable internal state.

ObservatoryProbeDeadline

type ObservatoryProbeDeadline interface {
    ObservationProbeDeadline() time.Duration
}

This reports the longest expected time before a scheduled Observatory can publish its initial result. Batch callers use ProbeOutboundsDeadline instead.

The interface is implemented by both the regular Observatory and BurstObservatory.

One-shot probing behavior

The BurstObservatory implementation now supports one-shot batches with the following behavior:

  • One Xray instance probes all requested outbounds.
  • Work is bounded by maxConcurrency.
  • Samples for a given outbound are collected serially by its worker.
  • Multiple outbounds can be measured concurrently.
  • Duplicate tags are normalized.
  • Empty tags, missing handlers, invalid concurrency, invalid sample counts, and excessive retained work are rejected before the batch begins.
  • Only one batch may run on an observer at a time.
  • A batch cannot overlap a manual Check.
  • Closing the observer cancels and waits for the active batch.

A one-shot observer must be configured without scheduled subject selectors. Scheduled observation and one-shot probing are intentionally kept mutually exclusive so their result sets and lifecycles cannot race.

The current implementation also applies defensive limits to active workers, samples per outbound, and total retained measurements. These are safety limits for untrusted or erroneous embedder parameters, not recommended operating values.

Progressive and partial results

One-shot probing does not wait for every outbound to finish before exposing useful information.

After each completed sample:

  1. The current batch result is updated.
  2. A coalesced notification is published.
  3. The embedder may call GetObservation.
  4. The user interface can immediately display the current average and availability information.

The batch begins with an empty result set. There is no previous batch snapshot: only measurements produced by the current invocation are exposed. Outbounds that have not produced a sample yet are absent.

Samples used by the one-shot batch are non-expiring because the intended lifecycle is:

  1. Create one dedicated Xray instance.
  2. Run one batch.
  3. consume its final or partial snapshot;
  4. discard the instance or containing process.

After ProbeOutbounds returns, GetObservation retains every sample completed by that invocation:

  • A nil error means the requested batch completed.
  • A non-nil error means the result may be partial.
  • Cancellation preserves samples completed before cancellation.
  • Core shutdown preserves already published samples.
  • Network loss preserves completed measurements and does not mark all unfinished outbounds as dead.

This makes partial progress useful while avoiding misleading synthetic failures.

Network-loss handling

A failed outbound and an unavailable underlying network are different conditions.

When an outbound probe fails and a connectivity check is configured, the implementation checks direct connectivity using the same cancellable instance context. If the underlying network is unavailable:

  • The remaining batch is cancelled.
  • Completed samples are retained.
  • Unfinished outbounds are not synthesized as failed.
  • ErrObservatoryProbeNetworkUnavailable is returned.

This allows an embedding application to report that the test was interrupted by network loss rather than incorrectly presenting every remaining proxy as unavailable.

Probe and connectivity dials honor cancellation at their individual deadlines, including tagged outbound dials. This prevents timed-out requests from remaining active after their result is no longer useful.

Deadline calculation

Batch deadlines are derived from the actual probing parameters:

  • Configured per-request timeout.
  • Number of unique outbound tags.
  • Maximum concurrency.
  • Number of samples per outbound.
  • Number of worker waves.
  • An additional connectivity-check timeout where applicable.

Overflow and invalid values are rejected.

Scheduled Observatory deadlines are also exposed for regular and burst implementations. This gives downstream applications a configuration-aware safety boundary rather than forcing them to guess how long an initial observation should take.

Warm-start support

The notification and scheduled-deadline additions are also required by downstream route warm-start implementations.

A mobile application may remember the last viable policy-group target for a particular network. After the underlying network changes, it can temporarily restore that target while the newly created core's Observatory performs its first fresh probe cycle.

A correct warm-start flow requires two pieces of information from Xray:

  1. An event indicating that Observatory results actually changed.
  2. A configuration-derived upper bound for the initial probe cycle.

ObservatoryUpdateNotifier allows the embedder to react when a real result changes instead of polling Observatory state every few hundred milliseconds. After a notification, the embedder reevaluates the balancer, stores the fresh viable target, and releases its temporary warm-route override.

ObservatoryProbeDeadline gives the embedder a sensible fallback deadline if no viable update is produced. For regular Observatory it accounts for concurrent or sequential probing. For BurstObservatory it reflects the configured health-ping timeout and optional connectivity check.

The update dispatcher is instance-scoped, supports multiple subscribers, coalesces unread notifications, and cannot be blocked by a slow consumer.

These APIs do not implement or persist a route cache inside Xray. Network identification and cache policy remain the embedding application's responsibility.

Compatibility

  • The existing Observatory and BurstObservatory interfaces retain their existing methods.
  • Batch probing, notifications, and deadline reporting are exposed through optional extension interfaces.
  • Existing embedders do not need to adopt the new APIs.
  • Existing scheduled Observatory and BurstObservatory operation remains available.
  • One-shot batch state is isolated from scheduled and manual probe state.
  • GetObservation returns stable snapshots rather than exposing mutable internal result objects.
  • No persistent cache or new configuration schema is introduced.

This PR is independent of XTLS/Xray-core#6483, whose reduced scope concerns instance-owned system dialer state.

Downstream use

This work is intended as the Xray-core prerequisite for:

AndroidLibXrayLite wraps the batch API for mobile embedders. v2rayNG uses it in one focused disposable Android process containing one Xray instance, allowing multiple profiles and policy-group candidates to be probed concurrently without creating one process per outbound.

The same API can be used by other desktop, mobile, or embedded clients that need efficient bulk outbound measurements.

Testing

The implementation includes coverage for:

  • Bounded worker concurrency.
  • Multiple samples per outbound.
  • Parameter and outbound-tag validation.
  • Batch deadline calculation and overflow handling.
  • Progressive update delivery.
  • Subscriber isolation and unsubscribe behavior.
  • Stable observation snapshots.
  • Empty batches.
  • Cancellation and core shutdown.
  • Partial-result retention.
  • Underlying network loss.
  • Per-probe deadline cancellation.
  • Isolation from scheduled and manual probes.
  • Non-expiring one-shot samples.
  • Correct RTT statistics on 32-bit targets.

Focused Observatory and extension tests pass, including race-detector runs for the BurstObservatory and extension packages. A release-style Xray executable also builds successfully.

Expose instance-scoped observatory update subscriptions so embedders can react when regular or burst probe results change without polling.

Report the effective initial probe-cycle deadline, allowing temporary routing overrides to remain active long enough for a fresh result. Clone regular observation snapshots under lock and emit notifications only after result locks are released.

Add coverage for subscription lifecycle, regular and burst updates, and configured probe deadlines.
Expose an embedder-facing batch probe interface so third-party applications can compare many outbound handlers through one running Xray instance instead of creating a core per outbound.

Validate every tag before dialing, bound concurrency across outbounds, keep samples for each outbound sequential, and publish the completed result set atomically. Make batches cancellable, mutually exclusive with scheduled observation, and safe to close from an update listener.

Cover concurrency, sampling, replacement, failed probes, cancellation, overlap, missing handlers, lifecycle shutdown, and synchronous listener cleanup.
Embedder cancellation contexts do not carry Xray's private instance value, so deriving tagged probe dials from the caller context makes the tagged dialer reject every real request. Keep the observer-owned context as the probe parent and bridge caller cancellation into it instead. Preserve the caller's cancellation error and cover the value propagation that real tagged dialing depends on.
A failed outbound probe followed by a failed direct connectivity check identifies an unavailable underlying network, not a dead outbound. Abort the batch with a public sentinel error, cancel the remaining workers, and leave the last complete observation untouched instead of atomically publishing a misleading all-failed snapshot. Cover the error, notification, and snapshot-preservation behavior.
An empty outbound list is a valid completed batch, but the early success path previously retained and republished the prior result map. Replace it with an explicit empty snapshot after cancellation checks, so GetObservation always describes the batch that just completed and never exposes stale routes as fresh results.
Go's HTTP transport may detach DialContext from request cancellation so an in-progress dial can be reused, allowing timed-out probe dials to outlive their worker slot. Create a dedicated transport and timeout-bound Xray context for every measurement, keep that context through the complete response, and cancel it only when the measurement ends. This preserves instance values while preventing lingering dials from escaping the configured batch concurrency bound.
BurstObservatory.Check can be invoked independently of scheduled selectors, including from Xray's reverse path, so it could mutate results while an embedder batch was building or publishing. Track active manual checks and the synchronous publication window under the existing lifecycle lock. Reject overlapping batches and drop manual checks during batch execution/publication without holding locks across probes or callbacks, preserving atomic snapshots and avoiding Close/listener deadlocks.
A burst observer's initial-cycle deadline describes one concurrent sample and cannot safely bound a finite batch with many tags or repeated samples. Extend the batch probe contract with a dedicated deadline calculation based on unique tags, worker waves, sequential samples, configured request timeout, and the optional connectivity check. Share argument normalization with execution, reject duration overflow, and keep the scheduled-observer deadline semantics unchanged.
Scheduled burst samples are intentionally aged out, but applying that policy to a finite batch lets early tags expire while later tags are still being probed. A successful batch can then publish a snapshot that no longer contains all requested measurements.

Treat non-positive result validity as non-expiring and use it for one-shot batches. The completed snapshot now remains coherent until a later batch replaces it or the observer is closed.
Health-ping averaging converted the nanosecond sum through int. On ARMv7, valid measurements whose total exceeds roughly 2.1 seconds wrap before division and can produce a negative or otherwise corrupt delay.

Divide the int64-backed time.Duration directly and cover multi-second samples with a regression test that also runs successfully under GOARCH=386.
Calling arbitrary subscriber functions synchronously lets a slow or panicking embedder extend probe deadlines, overlap core teardown, and serialize otherwise concurrent observatory work. No callback contract can safely stop a function that refuses to return.

Publish coalesced signals through buffered subscription channels instead. Notification is now nonblocking, Xray never executes subscriber code, concurrent updates are serialized by the channel, and observer shutdown closes every subscription explicitly.
The batch API previously accepted any positive concurrency and sample counts. Those values directly size worker pools and retained RTT storage, while ProbeOutbounds could execute plans that ProbeOutboundsDeadline rejected for duration overflow.

Apply explicit worker, per-outbound sample, and total-measurement ceilings, and share deadline validation with the execution path. Invalid or overflowing plans now fail before starting any network work or allocating their result snapshot.
Holding every result until the slowest outbound finishes makes large batches appear stalled, even when useful latency measurements are already available.

Expose an isolated in-progress observation view and signal after each completed sample. Successful batches promote that view atomically to the stable snapshot, while cancellation or network loss discards it and signals subscribers to restore the previous complete results.
A one-shot probe runs in a disposable process, so it has no meaningful prior batch to restore when cancellation or network loss interrupts the current run.

Keep every measurement completed by the current batch, omit unfinished outbounds, and use the returned error to mark the result set incomplete. This preserves useful UI data without manufacturing failures for probes that never finished.
One-shot probes run through Observer, which already validates inputs and serializes batch and manual activity. HealthPing repeated those checks and maintained a separate active snapshot even though completed samples are now retained on every exit path.

Keep batch policy at the Observer boundary, replace the current result map when execution starts, and publish each sample directly. Remove the redundant batch lock, active-result wrapper, and success-only final notification while leaving standard Observatory and scheduled BurstObservatory operation unchanged.

Update tests to use production update wiring and model one disposable probe batch instead of relying on previous or repeated batches.
@Fangliding

Copy link
Copy Markdown
Member

不打算扩展这个接口了
不要把一堆AI pr拿上来

@Fangliding Fangliding closed this Jul 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants