From b4b6c9e9a752c60b5147f7536d962b7d03261b1a Mon Sep 17 00:00:00 2001 From: Steven Miller Date: Wed, 29 Apr 2026 14:06:11 -0400 Subject: [PATCH 1/2] Speed up instance name resolution --- lib/dns/server.go | 12 ++++- lib/instances/fork.go | 20 -------- lib/instances/ingress_resolver.go | 27 +++++++++++ lib/instances/manager.go | 71 ++++++++++----------------- lib/instances/query.go | 81 +++++++++++++++++++++++++++++++ 5 files changed, 144 insertions(+), 67 deletions(-) diff --git a/lib/dns/server.go b/lib/dns/server.go index 74f1814f..8e1c9c2c 100644 --- a/lib/dns/server.go +++ b/lib/dns/server.go @@ -41,6 +41,10 @@ type InstanceResolver interface { ResolveInstanceIP(ctx context.Context, nameOrID string) (string, error) } +type dnsInstanceResolver interface { + ResolveInstanceIPForDNS(ctx context.Context, nameOrID string) (string, error) +} + // Server provides DNS-based instance resolution for Caddy. // It listens on a local port and responds to A record queries // for instances in the form ".hypeman.internal". @@ -180,7 +184,13 @@ func (s *Server) handleAQuery(m *dns.Msg, q dns.Question) { ctx, cancel := context.WithTimeout(context.Background(), resolverTimeout) defer cancel() - ip, err := s.resolver.ResolveInstanceIP(ctx, instanceName) + var ip string + var err error + if resolver, ok := s.resolver.(dnsInstanceResolver); ok { + ip, err = resolver.ResolveInstanceIPForDNS(ctx, instanceName) + } else { + ip, err = s.resolver.ResolveInstanceIP(ctx, instanceName) + } if err != nil { s.log.Debug("DNS resolution failed", "instance", instanceName, "error", err) // Return NXDOMAIN by not adding any answer records diff --git a/lib/instances/fork.go b/lib/instances/fork.go index c7dbef65..4ce7ee6e 100644 --- a/lib/instances/fork.go +++ b/lib/instances/fork.go @@ -7,7 +7,6 @@ import ( "fmt" "hash/crc32" "os" - "path/filepath" "strings" "time" @@ -368,25 +367,6 @@ func validateForkVolumeSafety(volumes []VolumeAttachment) error { return nil } -func (m *manager) instanceNameExists(name string) (bool, error) { - metaFiles, err := m.listMetadataFiles() - if err != nil { - return false, err - } - - for _, metaFile := range metaFiles { - id := filepath.Base(filepath.Dir(metaFile)) - meta, err := m.loadMetadata(id) - if err != nil { - continue - } - if meta.Name == name { - return true, nil - } - } - return false, nil -} - func resolveForkTargetState(requested State, sourceState State) (State, error) { if requested == "" { switch sourceState { diff --git a/lib/instances/ingress_resolver.go b/lib/instances/ingress_resolver.go index 47d9200a..f95da792 100644 --- a/lib/instances/ingress_resolver.go +++ b/lib/instances/ingress_resolver.go @@ -5,6 +5,8 @@ import ( "fmt" ) +const dnsMinIDPrefixLength = 8 + // IngressResolver provides instance resolution for the ingress package. // It implements ingress.InstanceResolver interface without importing the ingress package // to avoid import cycles. @@ -12,6 +14,10 @@ type IngressResolver struct { manager Manager } +type minPrefixInstanceManager interface { + getInstanceWithMinIDPrefix(ctx context.Context, idOrName string, minPrefixLength int) (*Instance, error) +} + // NewIngressResolver creates a new IngressResolver that wraps an instance manager. func NewIngressResolver(manager Manager) *IngressResolver { return &IngressResolver{manager: manager} @@ -24,6 +30,27 @@ func (r *IngressResolver) ResolveInstanceIP(ctx context.Context, nameOrID string return "", fmt.Errorf("instance not found: %s", nameOrID) } + return resolvedInstanceIP(inst, nameOrID) +} + +// ResolveInstanceIPForDNS resolves an instance IP for DNS lookups. +// DNS keeps exact ID/name behavior, but requires longer ID prefixes to avoid +// accidental broad matches from short DNS labels. +func (r *IngressResolver) ResolveInstanceIPForDNS(ctx context.Context, nameOrID string) (string, error) { + manager, ok := r.manager.(minPrefixInstanceManager) + if !ok { + return r.ResolveInstanceIP(ctx, nameOrID) + } + + inst, err := manager.getInstanceWithMinIDPrefix(ctx, nameOrID, dnsMinIDPrefixLength) + if err != nil { + return "", fmt.Errorf("instance not found: %s", nameOrID) + } + + return resolvedInstanceIP(inst, nameOrID) +} + +func resolvedInstanceIP(inst *Instance, nameOrID string) (string, error) { // Check if instance has network enabled if !inst.NetworkEnabled { return "", fmt.Errorf("instance %s has no network configured", nameOrID) diff --git a/lib/instances/manager.go b/lib/instances/manager.go index 495d8a4e..61774dad 100644 --- a/lib/instances/manager.go +++ b/lib/instances/manager.go @@ -299,6 +299,15 @@ func (m *manager) maybePersistBootMarkers(ctx context.Context, id string) { m.persistBootMarkers(ctx, id) } +func (m *manager) finalizeResolvedInstance(ctx context.Context, inst *Instance) { + if inst.State == StateStopped && inst.ExitCode != nil { + m.maybePersistExitInfo(ctx, inst.Id) + } + if (inst.State == StateRunning || inst.State == StateInitializing) && inst.BootMarkersHydrated { + m.maybePersistBootMarkers(ctx, inst.Id) + } +} + func (m *manager) recordImageUsage(ctx context.Context, imageInfo *images.Image) { if m.imageUsageRecorder == nil || imageInfo == nil { return @@ -499,66 +508,36 @@ func (m *manager) ListInstances(ctx context.Context, filter *ListInstancesFilter // Lookup order: exact ID match -> exact name match -> ID prefix match. // Returns ErrAmbiguousName if prefix matches multiple instances. func (m *manager) GetInstance(ctx context.Context, idOrName string) (*Instance, error) { + return m.getInstanceWithMinIDPrefix(ctx, idOrName, 1) +} + +func (m *manager) getInstanceWithMinIDPrefix(ctx context.Context, idOrName string, minPrefixLength int) (*Instance, error) { // 1. Try exact ID match first (most common case) lock := m.getInstanceLock(idOrName) lock.RLock() inst, err := m.getInstance(ctx, idOrName) lock.RUnlock() if err == nil { - // If VM is stopped with unpersisted exit info, persist under write lock. - // This handles the "app exited on its own" case where stopInstance wasn't called. - if inst.State == StateStopped && inst.ExitCode != nil { - m.maybePersistExitInfo(ctx, inst.Id) - } - if (inst.State == StateRunning || inst.State == StateInitializing) && inst.BootMarkersHydrated { - m.maybePersistBootMarkers(ctx, inst.Id) - } + m.finalizeResolvedInstance(ctx, inst) return inst, nil } - // 2. List all instances for name and prefix matching - instances, err := m.ListInstances(ctx, nil) + // 2. Resolve exact name or ID prefix from metadata only, then hydrate the + // single matched instance. + meta, err := m.findInstanceMetadataByNameOrIDPrefix(idOrName, minPrefixLength) if err != nil { return nil, err } - // 3. Try exact name match - var nameMatches []Instance - for _, inst := range instances { - if inst.Name == idOrName { - nameMatches = append(nameMatches, inst) - } - } - if len(nameMatches) == 1 { - inst := &nameMatches[0] - if inst.State == StateStopped && inst.ExitCode != nil { - m.maybePersistExitInfo(ctx, inst.Id) - } - return inst, nil - } - if len(nameMatches) > 1 { - return nil, ErrAmbiguousName - } - - // 4. Try ID prefix match - var prefixMatches []Instance - for _, inst := range instances { - if len(idOrName) > 0 && len(inst.Id) >= len(idOrName) && inst.Id[:len(idOrName)] == idOrName { - prefixMatches = append(prefixMatches, inst) - } - } - if len(prefixMatches) == 1 { - inst := &prefixMatches[0] - if inst.State == StateStopped && inst.ExitCode != nil { - m.maybePersistExitInfo(ctx, inst.Id) - } - return inst, nil - } - if len(prefixMatches) > 1 { - return nil, ErrAmbiguousName + resolvedLock := m.getInstanceLock(meta.Id) + resolvedLock.RLock() + inst, err = m.getInstance(ctx, meta.Id) + resolvedLock.RUnlock() + if err != nil { + return nil, err } - - return nil, ErrNotFound + m.finalizeResolvedInstance(ctx, inst) + return inst, nil } // StreamInstanceLogs streams instance logs from the specified source diff --git a/lib/instances/query.go b/lib/instances/query.go index 0a38771c..2986f175 100644 --- a/lib/instances/query.go +++ b/lib/instances/query.go @@ -649,6 +649,87 @@ func (m *manager) listInstances(ctx context.Context) ([]Instance, error) { return result, nil } +func (m *manager) findInstanceMetadataByExactName(name string) (*metadata, error) { + files, err := m.listMetadataFiles() + if err != nil { + return nil, err + } + + for _, file := range files { + id := filepath.Base(filepath.Dir(file)) + meta, err := m.loadMetadata(id) + if err != nil { + continue + } + if meta.Name == name { + return meta, nil + } + } + return nil, ErrNotFound +} + +func (m *manager) findInstanceMetadataByNameOrIDPrefix(idOrName string, minPrefixLength int) (*metadata, error) { + files, err := m.listMetadataFiles() + if err != nil { + return nil, err + } + if minPrefixLength < 1 { + minPrefixLength = 1 + } + + var nameMatch *metadata + var prefixMatch *metadata + nameMatches := 0 + prefixMatches := 0 + + for _, file := range files { + id := filepath.Base(filepath.Dir(file)) + meta, err := m.loadMetadata(id) + if err != nil { + continue + } + + if meta.Name == idOrName { + nameMatches++ + if nameMatches == 1 { + nameMatch = meta + } + } + + if len(idOrName) >= minPrefixLength && strings.HasPrefix(meta.Id, idOrName) { + prefixMatches++ + if prefixMatches == 1 { + prefixMatch = meta + } + } + } + + if nameMatches == 1 { + return nameMatch, nil + } + if nameMatches > 1 { + return nil, ErrAmbiguousName + } + if prefixMatches == 1 { + return prefixMatch, nil + } + if prefixMatches > 1 { + return nil, ErrAmbiguousName + } + return nil, ErrNotFound +} + +func (m *manager) instanceNameExists(name string) (bool, error) { + _, err := m.findInstanceMetadataByExactName(name) + if err == nil { + return true, nil + } + if err == ErrNotFound { + return false, nil + } + return false, err +} + // getInstance returns a single instance by ID func (m *manager) getInstance(ctx context.Context, id string) (*Instance, error) { log := logger.FromContext(ctx) From 58516810e3993b55937b451b606a44f35b4ef62f Mon Sep 17 00:00:00 2001 From: Steven Miller Date: Wed, 29 Apr 2026 14:16:14 -0400 Subject: [PATCH 2/2] Simplify DNS instance resolution --- lib/dns/server.go | 14 ++------------ lib/instances/ingress_resolver.go | 18 +----------------- 2 files changed, 3 insertions(+), 29 deletions(-) diff --git a/lib/dns/server.go b/lib/dns/server.go index 8e1c9c2c..eeab54c4 100644 --- a/lib/dns/server.go +++ b/lib/dns/server.go @@ -37,14 +37,10 @@ const ( // InstanceResolver provides instance IP resolution. // This interface is implemented by the instances package. type InstanceResolver interface { - // ResolveInstanceIP resolves an instance name or ID to its IP address. + // ResolveInstanceIP resolves an instance name or ID to its IP address for DNS. ResolveInstanceIP(ctx context.Context, nameOrID string) (string, error) } -type dnsInstanceResolver interface { - ResolveInstanceIPForDNS(ctx context.Context, nameOrID string) (string, error) -} - // Server provides DNS-based instance resolution for Caddy. // It listens on a local port and responds to A record queries // for instances in the form ".hypeman.internal". @@ -184,13 +180,7 @@ func (s *Server) handleAQuery(m *dns.Msg, q dns.Question) { ctx, cancel := context.WithTimeout(context.Background(), resolverTimeout) defer cancel() - var ip string - var err error - if resolver, ok := s.resolver.(dnsInstanceResolver); ok { - ip, err = resolver.ResolveInstanceIPForDNS(ctx, instanceName) - } else { - ip, err = s.resolver.ResolveInstanceIP(ctx, instanceName) - } + ip, err := s.resolver.ResolveInstanceIP(ctx, instanceName) if err != nil { s.log.Debug("DNS resolution failed", "instance", instanceName, "error", err) // Return NXDOMAIN by not adding any answer records diff --git a/lib/instances/ingress_resolver.go b/lib/instances/ingress_resolver.go index f95da792..17c2bf7a 100644 --- a/lib/instances/ingress_resolver.go +++ b/lib/instances/ingress_resolver.go @@ -25,21 +25,9 @@ func NewIngressResolver(manager Manager) *IngressResolver { // ResolveInstanceIP resolves an instance name, ID, or ID prefix to its IP address. func (r *IngressResolver) ResolveInstanceIP(ctx context.Context, nameOrID string) (string, error) { - inst, err := r.manager.GetInstance(ctx, nameOrID) - if err != nil { - return "", fmt.Errorf("instance not found: %s", nameOrID) - } - - return resolvedInstanceIP(inst, nameOrID) -} - -// ResolveInstanceIPForDNS resolves an instance IP for DNS lookups. -// DNS keeps exact ID/name behavior, but requires longer ID prefixes to avoid -// accidental broad matches from short DNS labels. -func (r *IngressResolver) ResolveInstanceIPForDNS(ctx context.Context, nameOrID string) (string, error) { manager, ok := r.manager.(minPrefixInstanceManager) if !ok { - return r.ResolveInstanceIP(ctx, nameOrID) + return "", fmt.Errorf("instance resolver does not support DNS-safe lookup") } inst, err := manager.getInstanceWithMinIDPrefix(ctx, nameOrID, dnsMinIDPrefixLength) @@ -47,10 +35,6 @@ func (r *IngressResolver) ResolveInstanceIPForDNS(ctx context.Context, nameOrID return "", fmt.Errorf("instance not found: %s", nameOrID) } - return resolvedInstanceIP(inst, nameOrID) -} - -func resolvedInstanceIP(inst *Instance, nameOrID string) (string, error) { // Check if instance has network enabled if !inst.NetworkEnabled { return "", fmt.Errorf("instance %s has no network configured", nameOrID)