detached a6ff697

feat(broker): reconcile configured observations

commit
a6ff6975dbc97e1361234a01ab8db767c0eccbf8
author
brice · · 2026-09-20 10:37 UTC
parent
b337f41
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +51 -5
M internal/usecase/broker/service.go +12 -3
@@ -312,9 +312,18 @@ func membershipError(err error) error {
312312 return ports.BrokerError{Code: code, Text: text, Cause: err}
313313 }
314314
315-// RequestReconcile remains a no-op until the observation service exposes a
316-// connection-safe reconcile seam.
317-func (s *Service) RequestReconcile(string) {}
315+// RequestReconcile schedules one coalesced observation for a configured
316+// endpoint. The registry validates membership and drops unknown endpoints, so
317+// this hint cannot expand authority or start an attachment.
318+func (s *Service) RequestReconcile(endpoint string) {
319+ s.mu.Lock()
320+ closed := s.closed
321+ s.mu.Unlock()
322+ if closed {
323+ return
324+ }
325+ s.registry.RequestProbe(endpoint)
326+}
318327
319328 // Close cancels and drains every owned resource exactly once: in-flight setup
320329 // opens and live streams stop, subscriptions close, the pool client is retired,
@@ -312,9 +312,18 @@ func membershipError(err error) error {
312 return ports.BrokerError{Code: code, Text: text, Cause: err}312 return ports.BrokerError{Code: code, Text: text, Cause: err}
313 }313 }
314 314
315-// RequestReconcile remains a no-op until the observation service exposes a315+// RequestReconcile schedules one coalesced observation for a configured
316-// connection-safe reconcile seam.316+// endpoint. The registry validates membership and drops unknown endpoints, so
317-func (s *Service) RequestReconcile(string) {}317+// this hint cannot expand authority or start an attachment.
318+func (s *Service) RequestReconcile(endpoint string) {
319+ s.mu.Lock()
320+ closed := s.closed
321+ s.mu.Unlock()
322+ if closed {
323+ return
324+ }
325+ s.registry.RequestProbe(endpoint)
326+}
318 327
319 // Close cancels and drains every owned resource exactly once: in-flight setup328 // Close cancels and drains every owned resource exactly once: in-flight setup
320 // opens and live streams stop, subscriptions close, the pool client is retired,329 // opens and live streams stop, subscriptions close, the pool client is retired,
M internal/usecase/broker/service_test.go +39 -2
@@ -531,7 +531,7 @@ func TestServiceShutdownTerminatesOwnedWork(t *testing.T) {
531531 // TestServiceImmutableMembershipRefusesEveryMutation proves the immutable
532532 // connection refuses all three membership mutations with the typed immutable
533533 // code 12 and the ports sentinel before any durable authority moves: no CAS is
534-// attempted, no publication changes, and reconcile stays a no-op.
534+// attempted and no publication changes.
535535 func TestServiceImmutableMembershipRefusesEveryMutation(t *testing.T) {
536536 store := newMembershipStore()
537537 authority, registry, _, _ := newServiceMembershipFixture(t, store, MembershipImmutable)
@@ -555,7 +555,44 @@ func TestServiceImmutableMembershipRefusesEveryMutation(t *testing.T) {
555555 require.Equal(t, before, registry.Snapshot(), "a refused mutation must not touch authority")
556556
557557 service.RequestReconcile("new@host:22")
558- require.Equal(t, before, registry.Snapshot(), "reconcile must be a no-op")
558+ require.Equal(t, before, registry.Snapshot(), "an unknown reconcile hint cannot change authority")
559+}
560+
561+func TestServiceRequestReconcileDelegatesToRegistry(t *testing.T) {
562+ clock := newManualClock(time.Unix(0, 0))
563+ store := newTestStore()
564+ registration := registration(t, "user@host:22", 1)
565+ store.hosts = ports.BrokerHosts{Revision: 1, Hosts: []ports.BrokerHostRecord{{Registration: registration, Pinned: true, Policy: poolPolicy()}}}
566+ probe := newTestProbe(2)
567+ registry, err := NewRegistry(1, store, probe, clock, nil)
568+ require.NoError(t, err)
569+ registry.jitter = identityJitter
570+ authority, _, _, _ := composeTestAuthority(t, 1, registry, immediateConnector, clock)
571+ service, err := authority.AdmitClient(context.Background())
572+ require.NoError(t, err)
573+ t.Cleanup(func() { require.NoError(t, service.Close()) })
574+
575+ var initial probeCall
576+ require.Eventually(t, func() bool {
577+ select {
578+ case initial = <-probe.calls:
579+ return true
580+ default:
581+ return false
582+ }
583+ }, time.Second, time.Millisecond)
584+ initial.result <- probeAnswer{snapshot: ports.BrokerDaemonObservation{Identity: "daemon", Incarnation: ports.BrokerDaemonIncarnation{1}, ProtocolVersion: 1, Availability: domain.RemoteAvailabilityReachable}}
585+ service.RequestReconcile(registration.Endpoint)
586+ var reconciled probeCall
587+ require.Eventually(t, func() bool {
588+ select {
589+ case reconciled = <-probe.calls:
590+ return true
591+ default:
592+ return false
593+ }
594+ }, time.Second, time.Millisecond)
595+ require.Equal(t, registration, reconciled.registration)
559596 }
560597
561598 // TestServiceMutableMembershipDelegatesOnce proves an admitted mutable
@@ -531,7 +531,7 @@ func TestServiceShutdownTerminatesOwnedWork(t *testing.T) {
531 // TestServiceImmutableMembershipRefusesEveryMutation proves the immutable531 // TestServiceImmutableMembershipRefusesEveryMutation proves the immutable
532 // connection refuses all three membership mutations with the typed immutable532 // connection refuses all three membership mutations with the typed immutable
533 // code 12 and the ports sentinel before any durable authority moves: no CAS is533 // code 12 and the ports sentinel before any durable authority moves: no CAS is
534-// attempted, no publication changes, and reconcile stays a no-op.534+// attempted and no publication changes.
535 func TestServiceImmutableMembershipRefusesEveryMutation(t *testing.T) {535 func TestServiceImmutableMembershipRefusesEveryMutation(t *testing.T) {
536 store := newMembershipStore()536 store := newMembershipStore()
537 authority, registry, _, _ := newServiceMembershipFixture(t, store, MembershipImmutable)537 authority, registry, _, _ := newServiceMembershipFixture(t, store, MembershipImmutable)
@@ -555,7 +555,44 @@ func TestServiceImmutableMembershipRefusesEveryMutation(t *testing.T) {
555 require.Equal(t, before, registry.Snapshot(), "a refused mutation must not touch authority")555 require.Equal(t, before, registry.Snapshot(), "a refused mutation must not touch authority")
556 556
557 service.RequestReconcile("new@host:22")557 service.RequestReconcile("new@host:22")
558- require.Equal(t, before, registry.Snapshot(), "reconcile must be a no-op")558+ require.Equal(t, before, registry.Snapshot(), "an unknown reconcile hint cannot change authority")
559+}
560+
561+func TestServiceRequestReconcileDelegatesToRegistry(t *testing.T) {
562+ clock := newManualClock(time.Unix(0, 0))
563+ store := newTestStore()
564+ registration := registration(t, "user@host:22", 1)
565+ store.hosts = ports.BrokerHosts{Revision: 1, Hosts: []ports.BrokerHostRecord{{Registration: registration, Pinned: true, Policy: poolPolicy()}}}
566+ probe := newTestProbe(2)
567+ registry, err := NewRegistry(1, store, probe, clock, nil)
568+ require.NoError(t, err)
569+ registry.jitter = identityJitter
570+ authority, _, _, _ := composeTestAuthority(t, 1, registry, immediateConnector, clock)
571+ service, err := authority.AdmitClient(context.Background())
572+ require.NoError(t, err)
573+ t.Cleanup(func() { require.NoError(t, service.Close()) })
574+
575+ var initial probeCall
576+ require.Eventually(t, func() bool {
577+ select {
578+ case initial = <-probe.calls:
579+ return true
580+ default:
581+ return false
582+ }
583+ }, time.Second, time.Millisecond)
584+ initial.result <- probeAnswer{snapshot: ports.BrokerDaemonObservation{Identity: "daemon", Incarnation: ports.BrokerDaemonIncarnation{1}, ProtocolVersion: 1, Availability: domain.RemoteAvailabilityReachable}}
585+ service.RequestReconcile(registration.Endpoint)
586+ var reconciled probeCall
587+ require.Eventually(t, func() bool {
588+ select {
589+ case reconciled = <-probe.calls:
590+ return true
591+ default:
592+ return false
593+ }
594+ }, time.Second, time.Millisecond)
595+ require.Equal(t, registration, reconciled.registration)
559 }596 }
560 597
561 // TestServiceMutableMembershipDelegatesOnce proves an admitted mutable598 // TestServiceMutableMembershipDelegatesOnce proves an admitted mutable

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 12

Tags 11

↑↓ moveenter openesc close