feat(master): suggest newer version tags for third-party images (#11)
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
// Package poller checks the registry for new digests of the tags the
|
||||
// services follow. A new digest becomes the service's available version, and
|
||||
// a service in auto mode gets an update job.
|
||||
// a service in auto mode gets an update job. It also lists the tags of the
|
||||
// repos whose followed tag looks like a version and suggests a newer one;
|
||||
// that suggestion never starts a job.
|
||||
package poller
|
||||
|
||||
import (
|
||||
@@ -11,14 +13,19 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/shcizo/package-updater/internal/imageref"
|
||||
"github.com/shcizo/package-updater/internal/master/events"
|
||||
"github.com/shcizo/package-updater/internal/master/jobs"
|
||||
"github.com/shcizo/package-updater/internal/master/model"
|
||||
"github.com/shcizo/package-updater/internal/master/tagver"
|
||||
)
|
||||
|
||||
// lookupTimeout bounds one registry lookup.
|
||||
const lookupTimeout = 30 * time.Second
|
||||
|
||||
// listTimeout bounds one tag listing, which may take several requests.
|
||||
const listTimeout = 60 * time.Second
|
||||
|
||||
// maxErrorBytes caps the registry error stored in services.last_poll_error.
|
||||
const maxErrorBytes = 500
|
||||
|
||||
@@ -28,6 +35,7 @@ type Store interface {
|
||||
GetService(ctx context.Context, id int64) (model.Service, error)
|
||||
SetAvailable(ctx context.Context, id int64, a model.Available) error
|
||||
SetLastTagDigest(ctx context.Context, id int64, digest string) error
|
||||
SetNewer(ctx context.Context, id int64, ref string, at time.Time) error
|
||||
SetPollError(ctx context.Context, ids []int64, msg string) error
|
||||
}
|
||||
|
||||
@@ -36,10 +44,11 @@ type Jobs interface {
|
||||
Start(ctx context.Context, svc model.Service, req jobs.StartRequest) (model.Job, error)
|
||||
}
|
||||
|
||||
// Registry looks up the digest a "repo:tag" ref points at. *registry.Client
|
||||
// satisfies it.
|
||||
// Registry looks up the digest a "repo:tag" ref points at and lists the tags
|
||||
// of a repo. *registry.Client satisfies it.
|
||||
type Registry interface {
|
||||
Digest(ctx context.Context, ref string) (string, error)
|
||||
Tags(ctx context.Context, repo string) ([]string, error)
|
||||
}
|
||||
|
||||
// Publisher receives change signals. *events.Broker satisfies it.
|
||||
@@ -49,7 +58,7 @@ type Publisher interface{ Publish(t events.Topic) }
|
||||
type Config struct {
|
||||
Interval time.Duration // time between the end of a cycle and the start of the next (POLL_INTERVAL)
|
||||
FirstDelay time.Duration // wait before the first cycle; 1 minute in production
|
||||
Workers int // registry lookups in flight at once; 4 in production
|
||||
Workers int // registry requests in flight at once; 4 in production
|
||||
}
|
||||
|
||||
// Poller runs polling cycles.
|
||||
@@ -112,13 +121,16 @@ type lookup struct {
|
||||
// cycleStats is what one cycle did, for the poll_cycle log line.
|
||||
type cycleStats struct {
|
||||
refs, ok, failed, suggested, jobs, skipped int
|
||||
reposListed, tagsFailed, newer int
|
||||
wrote bool
|
||||
}
|
||||
|
||||
// Cycle runs one poll over all services. It looks up every distinct followed
|
||||
// ref once, with at most Config.Workers lookups at a time, then records the
|
||||
// results. Writing results and starting jobs is not cut short by ctx ending
|
||||
// during the lookups, so a job that was marked is always started.
|
||||
// results. Then it lists the tags of each repo that has a service following a
|
||||
// version-like tag, once per repo, and records newer tags. Writing results and
|
||||
// starting jobs is not cut short by ctx ending during the lookups, so a job
|
||||
// that was marked is always started.
|
||||
func (p *Poller) Cycle(ctx context.Context) {
|
||||
began := p.now()
|
||||
svcs, err := p.store.ListServices(ctx)
|
||||
@@ -152,19 +164,20 @@ func (p *Poller) Cycle(ctx context.Context) {
|
||||
}
|
||||
p.record(write, l, byRef[l.ref], &stats)
|
||||
}
|
||||
p.syncNewer(ctx, write, svcs, &stats)
|
||||
if stats.wrote {
|
||||
p.pub.Publish(events.Services)
|
||||
}
|
||||
p.log.Info("poll_cycle", "refs", stats.refs, "ok", stats.ok, "failed", stats.failed,
|
||||
"suggested", stats.suggested, "jobs", stats.jobs, "skipped", stats.skipped,
|
||||
"repos_listed", stats.reposListed, "tags_failed", stats.tagsFailed, "newer", stats.newer,
|
||||
"duration_ms", p.now().Sub(began).Milliseconds())
|
||||
}
|
||||
|
||||
// lookupAll asks the registry for every ref, Config.Workers at a time. It
|
||||
// returns the answers in ref order and stops starting new lookups once ctx
|
||||
// is done; refs never looked up are missing from the result.
|
||||
func (p *Poller) lookupAll(ctx context.Context, refs []string) []lookup {
|
||||
results := make([]*lookup, len(refs))
|
||||
// forEach calls fn(i) for i in [0, n), Config.Workers at a time, and stops
|
||||
// starting new calls once ctx is done. It returns when all started calls
|
||||
// have returned.
|
||||
func (p *Poller) forEach(ctx context.Context, n int, fn func(i int)) {
|
||||
next := make(chan int)
|
||||
var wg sync.WaitGroup
|
||||
for w := 0; w < p.cfg.Workers; w++ {
|
||||
@@ -172,15 +185,12 @@ func (p *Poller) lookupAll(ctx context.Context, refs []string) []lookup {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for i := range next {
|
||||
lctx, cancel := context.WithTimeout(ctx, lookupTimeout)
|
||||
d, err := p.reg.Digest(lctx, refs[i])
|
||||
cancel()
|
||||
results[i] = &lookup{ref: refs[i], digest: d, err: err}
|
||||
fn(i)
|
||||
}
|
||||
}()
|
||||
}
|
||||
feed:
|
||||
for i := range refs {
|
||||
for i := 0; i < n; i++ {
|
||||
select {
|
||||
case next <- i:
|
||||
case <-ctx.Done():
|
||||
@@ -189,6 +199,19 @@ feed:
|
||||
}
|
||||
close(next)
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// lookupAll asks the registry for every ref, Config.Workers at a time. It
|
||||
// returns the answers in ref order and stops starting new lookups once ctx
|
||||
// is done; refs never looked up are missing from the result.
|
||||
func (p *Poller) lookupAll(ctx context.Context, refs []string) []lookup {
|
||||
results := make([]*lookup, len(refs))
|
||||
p.forEach(ctx, len(refs), func(i int) {
|
||||
lctx, cancel := context.WithTimeout(ctx, lookupTimeout)
|
||||
d, err := p.reg.Digest(lctx, refs[i])
|
||||
cancel()
|
||||
results[i] = &lookup{ref: refs[i], digest: d, err: err}
|
||||
})
|
||||
var out []lookup
|
||||
for _, r := range results {
|
||||
if r != nil {
|
||||
@@ -198,6 +221,109 @@ feed:
|
||||
return out
|
||||
}
|
||||
|
||||
// tagList is the registry's answer for the tags of one repo.
|
||||
type tagList struct {
|
||||
repo string
|
||||
tags []string
|
||||
err error
|
||||
}
|
||||
|
||||
// listAll lists the tags of every repo like lookupAll looks up refs.
|
||||
func (p *Poller) listAll(ctx context.Context, repos []string) []tagList {
|
||||
results := make([]*tagList, len(repos))
|
||||
p.forEach(ctx, len(repos), func(i int) {
|
||||
lctx, cancel := context.WithTimeout(ctx, listTimeout)
|
||||
tags, err := p.reg.Tags(lctx, repos[i])
|
||||
cancel()
|
||||
results[i] = &tagList{repo: repos[i], tags: tags, err: err}
|
||||
})
|
||||
var out []tagList
|
||||
for _, r := range results {
|
||||
if r != nil {
|
||||
out = append(out, *r)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// followedTag returns the repo and tag svc follows. ok is false unless svc is
|
||||
// polled and the tag can have a newer tag.
|
||||
func followedTag(svc model.Service) (repo, tag string, ok bool) {
|
||||
if SkipReason(svc) != "" {
|
||||
return "", "", false
|
||||
}
|
||||
ref := imageref.Parse(jobs.FollowedRef(svc.ActualRef))
|
||||
return ref.Repo, ref.Tag, tagver.Comparable(ref.Tag)
|
||||
}
|
||||
|
||||
// syncNewer lists the tags of every repo that has a service following a
|
||||
// version-like tag, once per repo, and stores the newer tag of each service.
|
||||
// A service whose tag cannot have a newer tag any more gets its suggestion
|
||||
// cleared; one whose repo could not be listed keeps it. Nothing is written
|
||||
// when the value is unchanged. It never starts a job, not even for an auto
|
||||
// service: a newer tag may be a major step the user has not chosen.
|
||||
func (p *Poller) syncNewer(ctx, write context.Context, svcs []model.Service, stats *cycleStats) {
|
||||
seen := map[string]bool{}
|
||||
var repos []string
|
||||
for _, svc := range svcs {
|
||||
if repo, _, ok := followedTag(svc); ok && svc.RemovedAt == nil && !seen[repo] {
|
||||
seen[repo] = true
|
||||
repos = append(repos, repo)
|
||||
}
|
||||
}
|
||||
lists := map[string][]string{}
|
||||
for _, l := range p.listAll(ctx, repos) {
|
||||
if l.err != nil {
|
||||
if ctx.Err() != nil {
|
||||
continue // cancelled mid-listing: not the registry's fault
|
||||
}
|
||||
stats.tagsFailed++
|
||||
p.log.Warn("poll_tags_failed", "repo", l.repo, "error", truncate(l.err.Error()))
|
||||
continue
|
||||
}
|
||||
stats.reposListed++
|
||||
lists[l.repo] = l.tags
|
||||
}
|
||||
|
||||
type repoTag struct{ repo, tag string }
|
||||
newest := map[repoTag]string{}
|
||||
for _, svc := range svcs {
|
||||
if svc.RemovedAt != nil {
|
||||
continue
|
||||
}
|
||||
want := ""
|
||||
if repo, tag, ok := followedTag(svc); ok {
|
||||
tags, listed := lists[repo]
|
||||
if !listed {
|
||||
if svc.NewerRef != "" {
|
||||
stats.newer++
|
||||
}
|
||||
continue // not listed this cycle: keep what is stored
|
||||
}
|
||||
key := repoTag{repo, tag}
|
||||
n, done := newest[key]
|
||||
if !done {
|
||||
n, _ = tagver.Newer(tag, tags)
|
||||
newest[key] = n
|
||||
}
|
||||
if n != "" {
|
||||
want = repo + ":" + n
|
||||
}
|
||||
}
|
||||
if want != "" {
|
||||
stats.newer++
|
||||
}
|
||||
if want == svc.NewerRef {
|
||||
continue
|
||||
}
|
||||
if err := p.store.SetNewer(write, svc.ID, want, p.now()); err != nil {
|
||||
p.log.Error("poll_set_newer_failed", "service_id", svc.ID, "ref", want, "error", err)
|
||||
continue
|
||||
}
|
||||
stats.wrote = true
|
||||
}
|
||||
}
|
||||
|
||||
// record stores one lookup result for the services that follow l.ref.
|
||||
func (p *Poller) record(ctx context.Context, l lookup, svcs []model.Service, stats *cycleStats) {
|
||||
if l.err != nil {
|
||||
|
||||
@@ -30,6 +30,12 @@ type pollErrorCall struct {
|
||||
msg string
|
||||
}
|
||||
|
||||
type newerCall struct {
|
||||
id int64
|
||||
ref string
|
||||
at time.Time
|
||||
}
|
||||
|
||||
type lastTagCall struct {
|
||||
id int64
|
||||
digest string
|
||||
@@ -40,6 +46,7 @@ type fakeStore struct {
|
||||
services []model.Service
|
||||
fresh map[int64]model.Service // what GetService returns instead of the listed row
|
||||
lastTag []lastTagCall
|
||||
newer []newerCall
|
||||
listErr error
|
||||
available []availableCall
|
||||
pollErrors []pollErrorCall
|
||||
@@ -86,6 +93,21 @@ func (f *fakeStore) SetAvailable(ctx context.Context, id int64, a model.Availabl
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetNewer records the call and, like the database, changes the row the
|
||||
// next cycle lists.
|
||||
func (f *fakeStore) SetNewer(ctx context.Context, id int64, ref string, at time.Time) error {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
f.ctxErrs = append(f.ctxErrs, ctx.Err())
|
||||
f.newer = append(f.newer, newerCall{id, ref, at})
|
||||
for i := range f.services {
|
||||
if f.services[i].ID == id {
|
||||
f.services[i].NewerRef = ref
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeStore) SetPollError(ctx context.Context, ids []int64, msg string) error {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
@@ -125,6 +147,36 @@ type fakeRegistry struct {
|
||||
started chan struct{} // receives a value whenever a lookup starts, if set
|
||||
release chan struct{} // lookups wait for it to be closed, if set
|
||||
onResult func() // called after a lookup computed its answer
|
||||
|
||||
tags map[string][]string // repo -> tags
|
||||
tagErrs map[string]error // repo -> error
|
||||
tagCalls []string
|
||||
tagStarted chan struct{} // receives a value whenever a tag listing starts, if set
|
||||
tagRelease chan struct{} // tag listings wait for it to be closed, if set
|
||||
onTags func() // called after a tag listing computed its answer
|
||||
}
|
||||
|
||||
func (f *fakeRegistry) Tags(ctx context.Context, repo string) ([]string, error) {
|
||||
f.mu.Lock()
|
||||
f.tagCalls = append(f.tagCalls, repo)
|
||||
f.mu.Unlock()
|
||||
if f.tagStarted != nil {
|
||||
f.tagStarted <- struct{}{}
|
||||
}
|
||||
if f.tagRelease != nil {
|
||||
select {
|
||||
case <-f.tagRelease:
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
}
|
||||
if f.onTags != nil {
|
||||
f.onTags()
|
||||
}
|
||||
if err := f.tagErrs[repo]; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return f.tags[repo], nil
|
||||
}
|
||||
|
||||
func (f *fakeRegistry) Digest(ctx context.Context, ref string) (string, error) {
|
||||
@@ -649,3 +701,190 @@ func TestSeedsTheTagDigestOfARunningService(t *testing.T) {
|
||||
assert.Empty(t, j.starts, "the rollback stays")
|
||||
assert.Len(t, st.lastTag, 1, "nothing more to seed")
|
||||
}
|
||||
|
||||
// newTagRegistry returns a fake registry that lists tags by repo.
|
||||
func newTagRegistry(tags map[string][]string) *fakeRegistry {
|
||||
return &fakeRegistry{digests: map[string]string{}, tags: tags}
|
||||
}
|
||||
|
||||
// newerSvc is a polled service running tag of repo that already runs the
|
||||
// registry's digest, so the digest phase writes nothing.
|
||||
func newerSvc(id int64, repo, tag string) model.Service {
|
||||
s := svc(id, repo+":"+tag)
|
||||
s.ActualDigest = "sha256:new"
|
||||
return s
|
||||
}
|
||||
|
||||
// newerRegistry is newTagRegistry that also answers sha256:new for refs.
|
||||
func newerRegistry(tags map[string][]string, refs ...string) *fakeRegistry {
|
||||
reg := newTagRegistry(tags)
|
||||
for _, r := range refs {
|
||||
reg.digests[r] = "sha256:new"
|
||||
}
|
||||
return reg
|
||||
}
|
||||
|
||||
func TestNewerTagSuggested(t *testing.T) {
|
||||
ref := repoB + ":1.27.3"
|
||||
st := &fakeStore{services: []model.Service{newerSvc(1, repoB, "1.27.3")}}
|
||||
reg := newerRegistry(map[string][]string{repoB: {"1.27.4", "1.28.0", "2.0.0", "latest", "1.28.1-rootless"}}, ref)
|
||||
p, pub := newPoller(st, &fakeJobs{}, reg, Config{})
|
||||
|
||||
p.Cycle(context.Background())
|
||||
|
||||
assert.Equal(t, []newerCall{{1, repoB + ":1.28.0", time.Date(2026, 10, 13, 12, 0, 0, 0, time.UTC)}}, st.newer)
|
||||
assert.Equal(t, []events.Topic{events.Services}, pub.topics)
|
||||
}
|
||||
|
||||
func TestOneTagListPerRepo(t *testing.T) {
|
||||
st := &fakeStore{services: []model.Service{
|
||||
newerSvc(1, repoB, "1.27.3"),
|
||||
newerSvc(2, repoB, "2.1.0"),
|
||||
newerSvc(3, repoB, "1.27.3"),
|
||||
}}
|
||||
reg := newerRegistry(map[string][]string{repoB: {"2.1.1", "1.27.4"}},
|
||||
repoB+":1.27.3", repoB+":2.1.0")
|
||||
p, _ := newPoller(st, &fakeJobs{}, reg, Config{})
|
||||
|
||||
p.Cycle(context.Background())
|
||||
|
||||
assert.Equal(t, []string{repoB}, reg.tagCalls)
|
||||
got := map[int64]string{}
|
||||
for _, c := range st.newer {
|
||||
got[c.id] = c.ref
|
||||
}
|
||||
assert.Equal(t, map[int64]string{1: repoB + ":1.27.4", 2: repoB + ":2.1.1", 3: repoB + ":1.27.4"}, got)
|
||||
}
|
||||
|
||||
func TestNoTagListWithoutComparableTag(t *testing.T) {
|
||||
built := newerSvc(5, repoB, "1.2.3")
|
||||
built.Built = true
|
||||
noDigest := newerSvc(6, repoB, "1.2.3")
|
||||
noDigest.ActualDigest = ""
|
||||
st := &fakeStore{services: []model.Service{
|
||||
newerSvc(1, repoB, "latest"),
|
||||
newerSvc(2, repoB, "7"),
|
||||
newerSvc(3, repoB, "pg16"),
|
||||
newerSvc(4, repoB+"@sha256:abc", ""),
|
||||
built,
|
||||
noDigest,
|
||||
}}
|
||||
st.services[3].ActualRef = repoB + "@sha256:abc"
|
||||
reg := newTagRegistry(map[string][]string{repoB: {"1.2.4"}})
|
||||
p, _ := newPoller(st, &fakeJobs{}, reg, Config{})
|
||||
|
||||
p.Cycle(context.Background())
|
||||
|
||||
assert.Empty(t, reg.tagCalls)
|
||||
assert.Empty(t, st.newer)
|
||||
}
|
||||
|
||||
func TestNewerClearedWhenNoCandidate(t *testing.T) {
|
||||
s := newerSvc(1, repoB, "1.27.3")
|
||||
s.NewerRef = repoB + ":1.27.4"
|
||||
st := &fakeStore{services: []model.Service{s}}
|
||||
reg := newerRegistry(map[string][]string{repoB: {"1.27.3", "2.0.0"}}, repoB+":1.27.3")
|
||||
p, _ := newPoller(st, &fakeJobs{}, reg, Config{})
|
||||
|
||||
p.Cycle(context.Background())
|
||||
|
||||
assert.Equal(t, []newerCall{{1, "", time.Date(2026, 10, 13, 12, 0, 0, 0, time.UTC)}}, st.newer)
|
||||
}
|
||||
|
||||
func TestNewerClearedWhenTagNoLongerComparable(t *testing.T) {
|
||||
moved := newerSvc(1, repoB, "latest")
|
||||
moved.NewerRef = repoB + ":1.27.4"
|
||||
built := newerSvc(2, repoB, "1.27.3")
|
||||
built.Built = true
|
||||
built.NewerRef = repoB + ":1.27.4"
|
||||
st := &fakeStore{services: []model.Service{moved, built}}
|
||||
reg := newerRegistry(map[string][]string{repoB: {"1.27.4"}}, repoB+":latest")
|
||||
p, pub := newPoller(st, &fakeJobs{}, reg, Config{})
|
||||
|
||||
p.Cycle(context.Background())
|
||||
|
||||
require.Len(t, st.newer, 2)
|
||||
assert.Equal(t, "", st.newer[0].ref)
|
||||
assert.Equal(t, "", st.newer[1].ref)
|
||||
assert.Empty(t, reg.tagCalls)
|
||||
assert.Equal(t, []events.Topic{events.Services}, pub.topics)
|
||||
}
|
||||
|
||||
func TestTagListErrorKeepsNewerAndPollError(t *testing.T) {
|
||||
s := newerSvc(1, repoB, "1.27.3")
|
||||
s.NewerRef = repoB + ":1.27.4"
|
||||
st := &fakeStore{services: []model.Service{s}}
|
||||
reg := newerRegistry(nil, repoB+":1.27.3")
|
||||
reg.tagErrs = map[string]error{repoB: errors.New("list x: registry answered 500 Internal Server Error")}
|
||||
p, pub := newPoller(st, &fakeJobs{}, reg, Config{})
|
||||
|
||||
p.Cycle(context.Background())
|
||||
|
||||
assert.Empty(t, st.newer, "the stored suggestion stays")
|
||||
assert.Empty(t, st.pollErrors, "a failed tag list is not a poll error")
|
||||
assert.Empty(t, pub.topics)
|
||||
}
|
||||
|
||||
func TestNewerIdempotent(t *testing.T) {
|
||||
s := newerSvc(1, repoB, "1.27.3")
|
||||
s.NewerRef = repoB + ":1.28.0"
|
||||
st := &fakeStore{services: []model.Service{s}}
|
||||
reg := newerRegistry(map[string][]string{repoB: {"1.28.0"}}, repoB+":1.27.3")
|
||||
p, pub := newPoller(st, &fakeJobs{}, reg, Config{})
|
||||
|
||||
p.Cycle(context.Background())
|
||||
|
||||
assert.Empty(t, st.newer)
|
||||
assert.Empty(t, pub.topics)
|
||||
}
|
||||
|
||||
func TestNewerNeverStartsJobs(t *testing.T) {
|
||||
s := newerSvc(1, repoB, "1.27.3")
|
||||
s.UpdateMode = model.ModeAuto
|
||||
st := &fakeStore{services: []model.Service{s}}
|
||||
reg := newerRegistry(map[string][]string{repoB: {"1.27.4"}}, repoB+":1.27.3")
|
||||
j := &fakeJobs{}
|
||||
p, _ := newPoller(st, j, reg, Config{})
|
||||
|
||||
p.Cycle(context.Background())
|
||||
|
||||
require.Len(t, st.newer, 1)
|
||||
assert.Empty(t, j.starts)
|
||||
}
|
||||
|
||||
func TestNewerWritesSurviveCancel(t *testing.T) {
|
||||
st := &fakeStore{services: []model.Service{newerSvc(1, repoB, "1.27.3")}}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
reg := newerRegistry(map[string][]string{repoB: {"1.27.4"}}, repoB+":1.27.3")
|
||||
reg.onTags = cancel
|
||||
p, _ := newPoller(st, &fakeJobs{}, reg, Config{})
|
||||
|
||||
p.Cycle(ctx)
|
||||
|
||||
require.Len(t, st.newer, 1)
|
||||
assert.NoError(t, st.ctxErrs[0], "the write does not see the cancelled context")
|
||||
}
|
||||
|
||||
func TestCancelledListKeepsNewer(t *testing.T) {
|
||||
s := newerSvc(1, repoB, "1.27.3")
|
||||
s.NewerRef = repoB + ":1.27.4"
|
||||
st := &fakeStore{services: []model.Service{s}}
|
||||
reg := newerRegistry(map[string][]string{repoB: {"1.28.0"}}, repoB+":1.27.3")
|
||||
reg.tagRelease = make(chan struct{})
|
||||
reg.tagStarted = make(chan struct{}, 1)
|
||||
p, pub := newPoller(st, &fakeJobs{}, reg, Config{})
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
done := make(chan struct{})
|
||||
go func() { p.Cycle(ctx); close(done) }()
|
||||
<-reg.tagStarted
|
||||
|
||||
cancel()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("Cycle did not return after cancel")
|
||||
}
|
||||
assert.Empty(t, st.newer)
|
||||
assert.Empty(t, pub.topics)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user