blob: d7b0a22e563a926c62fff35a7203bd07bd3c51d9 [file] [log] [blame]
// Copyright 2016 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ble
import (
"fmt"
"sync"
"time"
"v.io/v23/context"
"v.io/v23/discovery"
idiscovery "v.io/x/ref/lib/discovery"
)
// TODO(jhahn): Remove the limit on the number of advertisements per interface.
//
// The current plan is
// * if there is only one advertisement
// - publish all information except attachments as is
// * if there are more than one advertisements
// - publish the latest directory addresses, # of advertisements
// - if there are <= 8 advertisements,
// - publish each advertisements up to 2K (4 characteristics)
// * if it doesn't fit, publish only id, hash
// - if there are <= 64 advertisements,
// - publish a list of a pair of id and hash (24 bytes / <id, hash>)
//
// * all missed advertisements / information will be fetched from directory server.
type advertiser struct {
ctx *context.T
driver Driver
mu sync.Mutex
adRecords map[string]*adRecord // GUARDED_BY(mu)
}
type adRecord struct {
uuid idiscovery.Uuid
adinfos map[discovery.AdId][]byte
expiry time.Time
}
const (
uuidGcDelay = 10 * time.Minute
)
func (a *advertiser) addAd(adinfo *idiscovery.AdInfo) error {
encoded, err := encodeAdInfo(adinfo)
if err != nil {
return err
}
a.mu.Lock()
defer a.mu.Unlock()
a.gcLocked()
rec := a.adRecords[adinfo.Ad.InterfaceName]
if rec == nil {
rec = &adRecord{
uuid: newServiceUuid(adinfo.Ad.InterfaceName),
adinfos: make(map[discovery.AdId][]byte),
}
a.adRecords[adinfo.Ad.InterfaceName] = rec
} else {
if len(rec.adinfos) >= maxNumPackedServices {
return fmt.Errorf("too many advertisements per interface: %d > %d", len(rec.adinfos), maxNumPackedServices)
}
// Stop the current advertising and restart with a toggled uuid to avoid a new
// advertisement from being deduped by cache.
a.driver.RemoveService(rec.uuid.String())
toggleServiceUuid(rec.uuid)
rec.expiry = time.Time{}
}
rec.adinfos[adinfo.Ad.Id] = encoded
cs := packToCharacteristics(rec.adinfos)
err = a.driver.AddService(rec.uuid.String(), cs)
if err != nil {
rec.expiry = time.Now().Add(uuidGcDelay)
}
return err
}
func (a *advertiser) removeAd(adinfo *idiscovery.AdInfo) {
a.mu.Lock()
defer a.mu.Unlock()
a.gcLocked()
rec := a.adRecords[adinfo.Ad.InterfaceName]
if rec == nil {
return
}
a.driver.RemoveService(rec.uuid.String())
delete(rec.adinfos, adinfo.Ad.Id)
if len(rec.adinfos) == 0 {
rec.expiry = time.Now().Add(uuidGcDelay)
} else {
// Restart advertising with a toggled uuid to avoid the updated advertisement
// from being deduped by cache.
toggleServiceUuid(rec.uuid)
cs := packToCharacteristics(rec.adinfos)
if err := a.driver.AddService(rec.uuid.String(), cs); err != nil {
a.ctx.Error(err)
rec.expiry = time.Now().Add(uuidGcDelay)
}
}
}
func (a *advertiser) gcLocked() {
// Instead of asynchronous gc, we purge old entries in every call to addAd or removeAd
// for simplicity. We do not worry about purging all old entries since there will be
// only a handful of ads in practice.
now := time.Now()
for interfaceName, rec := range a.adRecords {
if rec.expiry.IsZero() || rec.expiry.After(now) {
continue
}
delete(a.adRecords, interfaceName)
}
}
func newAdvertiser(ctx *context.T, driver Driver) *advertiser {
return &advertiser{
ctx: ctx,
driver: driver,
adRecords: make(map[string]*adRecord),
}
}