Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions browse.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"context"
"fmt"
"net"
"time"
)

// BrowseEntry represents a discovered service instance.
Expand Down Expand Up @@ -91,6 +92,31 @@ func lookupType(ctx context.Context, service string, conn MDNSConn, add AddFunc,
}
}()

refreshTimer := time.NewTimer(time.Hour)
refreshTimer.Stop()

sendRefreshQuery := func() {
for _, iface := range MulticastInterfaces(ifaces...) {
q := &Query{msg: m, iface: iface}
if err := conn.SendQuery(q); err != nil {
log.Debug.Println("RefreshQuery:", err)
}
}
}

resetRefreshTimer := func() {
exp := cache.EarliestExpiration()
if exp.IsZero() {
refreshTimer.Stop()
return
}
remaining := time.Until(exp)
refreshAt := time.Duration(float64(remaining) * 0.8)
if refreshAt > 0 {
refreshTimer.Reset(refreshAt)
}
}

es := []*BrowseEntry{}
for {
select {
Expand All @@ -100,9 +126,13 @@ func lookupType(ctx context.Context, service string, conn MDNSConn, add AddFunc,
log.Debug.Println("SendQuery:", err)
}

case <-refreshTimer.C:
sendRefreshQuery()

case req := <-ch:
log.Debug.Printf("Receive message at %s\n%s\n", req.IfaceName(), req.msg)
cache.UpdateFrom(req)
resetRefreshTimer()
for _, srv := range cache.Services() {
if srv.ServiceName() != service {
continue
Expand Down
26 changes: 26 additions & 0 deletions cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
// Cache stores services in memory.
type Cache struct {
services map[string]*Service
earliestExpiry time.Time
}

// NewCache returns a new in-memory cache.
Expand All @@ -20,6 +21,24 @@ func NewCache() *Cache {
}
}

// EarliestExpiration returns the earliest expiration time across all cached services.
func (c *Cache) EarliestExpiration() time.Time {
return c.earliestExpiry
}

func (c *Cache) updateEarliestExpiry(exp time.Time) {
if c.earliestExpiry.IsZero() || exp.Before(c.earliestExpiry) {
c.earliestExpiry = exp
}
}

func (c *Cache) recalcEarliestExpiry() {
c.earliestExpiry = time.Time{}
for _, srv := range c.services {
c.updateEarliestExpiry(srv.expiration)
}
}

// Services returns a list of stored services.
func (c *Cache) Services() []*Service {
tmp := []*Service{}
Expand Down Expand Up @@ -55,6 +74,7 @@ func (c *Cache) UpdateFrom(req *Request) (adds []*Service, rmvs []*Service) {

entry.TTL = ttl
entry.expiration = time.Now().Add(ttl)
c.updateEarliestExpiry(entry.expiration)

case *dns.SRV:
ttl := time.Duration(rr.Hdr.Ttl) * time.Second
Expand All @@ -74,6 +94,7 @@ func (c *Cache) UpdateFrom(req *Request) (adds []*Service, rmvs []*Service) {
entry.SetHostname(rr.Target)
entry.TTL = ttl
entry.expiration = time.Now().Add(ttl)
c.updateEarliestExpiry(entry.expiration)
entry.Port = int(rr.Port)

case *dns.A:
Expand Down Expand Up @@ -112,6 +133,7 @@ func (c *Cache) UpdateFrom(req *Request) (adds []*Service, rmvs []*Service) {
entry.Text = text
entry.TTL = time.Duration(rr.Hdr.Ttl) * time.Second
entry.expiration = time.Now().Add(entry.TTL)
c.updateEarliestExpiry(entry.expiration)
}
default:
// ignore
Expand All @@ -134,6 +156,10 @@ func (c *Cache) removeExpired() []*Service {
}
}

if len(outdated) > 0 {
c.recalcEarliestExpiry()
}

return outdated
}

Expand Down
91 changes: 91 additions & 0 deletions cache_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package dnssd

import (
"testing"
"time"
)

func TestEarliestExpirationEmpty(t *testing.T) {
c := NewCache()
exp := c.EarliestExpiration()
if !exp.IsZero() {
t.Errorf("expected zero time for empty cache, got %v", exp)
}
}

func TestEarliestExpirationTracking(t *testing.T) {
c := NewCache()

later := time.Now().Add(10 * time.Second)
sooner := time.Now().Add(5 * time.Second)

s1 := newService("Test1._test._tcp.local.")
s1.expiration = later
c.services["t1"] = s1
c.updateEarliestExpiry(s1.expiration)

if !c.EarliestExpiration().Equal(later) {
t.Errorf("expected earliest to be %v, got %v", later, c.EarliestExpiration())
}

s2 := newService("Test2._test._tcp.local.")
s2.expiration = sooner
c.services["t2"] = s2
c.updateEarliestExpiry(s2.expiration)

if !c.EarliestExpiration().Equal(sooner) {
t.Errorf("expected earliest to be %v (sooner), got %v", sooner, c.EarliestExpiration())
}
}

func TestEarliestExpirationRecalcOnRemove(t *testing.T) {
c := NewCache()

s1 := newService("ServiceOne._test._tcp.local.")
s1.expiration = time.Now().Add(-1 * time.Second) // already expired
c.services["svc1"] = s1
c.updateEarliestExpiry(s1.expiration)

s2 := newService("ServiceTwo._test._tcp.local.")
s2.expiration = time.Now().Add(10 * time.Second)
c.services["svc2"] = s2
c.updateEarliestExpiry(s2.expiration)

if len(c.services) != 2 {
t.Fatalf("expected 2 services, got %d", len(c.services))
}

c.removeExpired()

if len(c.services) != 1 {
t.Fatalf("expected 1 service after removal, got %d", len(c.services))
}

// earliest should be s2 (the only remaining service)
if c.EarliestExpiration().IsZero() {
t.Error("earliest should not be zero with one service remaining")
}

remaining := time.Until(c.EarliestExpiration())
if remaining < 9*time.Second || remaining > 11*time.Second {
t.Errorf("expected ~10s remaining, got %v", remaining)
}
}

func TestEarliestExpirationAllRemoved(t *testing.T) {
c := NewCache()

s1 := newService("Gone._test._tcp.local.")
s1.expiration = time.Now().Add(-1 * time.Second)
c.services["gone"] = s1
c.updateEarliestExpiry(s1.expiration)

c.removeExpired()

if !c.EarliestExpiration().IsZero() {
t.Error("earliest should be zero after all services removed")
}
if len(c.services) != 0 {
t.Errorf("expected 0 services, got %d", len(c.services))
}
}