2023-04-10 15:36:59 +00:00
|
|
|
// Copyright (c) HashiCorp, Inc.
|
|
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
|
2020-04-30 13:13:00 +00:00
|
|
|
package volumewatcher
|
|
|
|
|
|
|
|
import (
|
|
|
|
"testing"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
memdb "github.com/hashicorp/go-memdb"
|
2022-03-15 12:42:43 +00:00
|
|
|
"github.com/hashicorp/nomad/ci"
|
2020-04-30 13:13:00 +00:00
|
|
|
"github.com/hashicorp/nomad/helper/testlog"
|
|
|
|
"github.com/hashicorp/nomad/nomad/mock"
|
|
|
|
"github.com/hashicorp/nomad/nomad/state"
|
|
|
|
"github.com/hashicorp/nomad/nomad/structs"
|
2022-11-01 20:53:10 +00:00
|
|
|
"github.com/shoenig/test/must"
|
2020-04-30 13:13:00 +00:00
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
)
|
|
|
|
|
|
|
|
// TestVolumeWatch_EnableDisable tests the watcher registration logic that needs
|
|
|
|
// to happen during leader step-up/step-down
|
|
|
|
func TestVolumeWatch_EnableDisable(t *testing.T) {
|
2022-03-15 12:42:43 +00:00
|
|
|
ci.Parallel(t)
|
2020-04-30 13:13:00 +00:00
|
|
|
|
|
|
|
srv := &MockRPCServer{}
|
|
|
|
srv.state = state.TestStateStore(t)
|
|
|
|
index := uint64(100)
|
|
|
|
|
2020-08-07 19:37:27 +00:00
|
|
|
watcher := NewVolumesWatcher(testlog.HCLogger(t), srv, "")
|
2022-04-04 14:46:45 +00:00
|
|
|
watcher.quiescentTimeout = 100 * time.Millisecond
|
2022-01-24 16:49:50 +00:00
|
|
|
watcher.SetEnabled(true, srv.State(), "")
|
2020-04-30 13:13:00 +00:00
|
|
|
|
|
|
|
plugin := mock.CSIPlugin()
|
2020-08-06 18:31:18 +00:00
|
|
|
node := testNode(plugin, srv.State())
|
2020-04-30 13:13:00 +00:00
|
|
|
alloc := mock.Alloc()
|
|
|
|
alloc.ClientStatus = structs.AllocClientStatusComplete
|
2022-04-04 14:46:45 +00:00
|
|
|
|
2020-08-06 18:31:18 +00:00
|
|
|
vol := testVolume(plugin, alloc, node.ID)
|
2020-04-30 13:13:00 +00:00
|
|
|
|
|
|
|
index++
|
2022-03-07 16:06:59 +00:00
|
|
|
err := srv.State().UpsertCSIVolume(index, []*structs.CSIVolume{vol})
|
2022-04-01 19:17:58 +00:00
|
|
|
require.NoError(t, err)
|
2020-04-30 13:13:00 +00:00
|
|
|
|
2022-04-04 14:46:45 +00:00
|
|
|
// need to have just enough of a volume and claim in place so that
|
|
|
|
// the watcher doesn't immediately stop and unload itself
|
2020-11-11 18:06:30 +00:00
|
|
|
claim := &structs.CSIVolumeClaim{
|
|
|
|
Mode: structs.CSIVolumeClaimGC,
|
|
|
|
State: structs.CSIVolumeClaimStateNodeDetached,
|
|
|
|
}
|
2020-04-30 13:13:00 +00:00
|
|
|
index++
|
|
|
|
err = srv.State().CSIVolumeClaim(index, vol.Namespace, vol.ID, claim)
|
2022-04-01 19:17:58 +00:00
|
|
|
require.NoError(t, err)
|
|
|
|
require.Eventually(t, func() bool {
|
2021-06-14 10:11:35 +00:00
|
|
|
watcher.wlock.RLock()
|
|
|
|
defer watcher.wlock.RUnlock()
|
2020-04-30 13:13:00 +00:00
|
|
|
return 1 == len(watcher.watchers)
|
|
|
|
}, time.Second, 10*time.Millisecond)
|
|
|
|
|
2022-01-24 16:49:50 +00:00
|
|
|
watcher.SetEnabled(false, nil, "")
|
2022-11-01 20:53:10 +00:00
|
|
|
watcher.wlock.RLock()
|
|
|
|
defer watcher.wlock.RUnlock()
|
2022-04-01 19:17:58 +00:00
|
|
|
require.Equal(t, 0, len(watcher.watchers))
|
2020-04-30 13:13:00 +00:00
|
|
|
}
|
|
|
|
|
2022-01-05 16:40:20 +00:00
|
|
|
// TestVolumeWatch_LeadershipTransition tests the correct behavior of
|
|
|
|
// claim reaping across leader step-up/step-down
|
|
|
|
func TestVolumeWatch_LeadershipTransition(t *testing.T) {
|
2022-03-15 12:42:43 +00:00
|
|
|
ci.Parallel(t)
|
2020-04-30 13:13:00 +00:00
|
|
|
|
|
|
|
srv := &MockRPCServer{}
|
|
|
|
srv.state = state.TestStateStore(t)
|
|
|
|
index := uint64(100)
|
|
|
|
|
2020-08-07 19:37:27 +00:00
|
|
|
watcher := NewVolumesWatcher(testlog.HCLogger(t), srv, "")
|
2022-04-04 14:46:45 +00:00
|
|
|
watcher.quiescentTimeout = 100 * time.Millisecond
|
2020-04-30 13:13:00 +00:00
|
|
|
|
|
|
|
plugin := mock.CSIPlugin()
|
2020-08-06 18:31:18 +00:00
|
|
|
node := testNode(plugin, srv.State())
|
2020-04-30 13:13:00 +00:00
|
|
|
alloc := mock.Alloc()
|
2022-01-27 15:39:08 +00:00
|
|
|
alloc.ClientStatus = structs.AllocClientStatusRunning
|
2020-08-06 18:31:18 +00:00
|
|
|
vol := testVolume(plugin, alloc, node.ID)
|
2020-04-30 13:13:00 +00:00
|
|
|
|
2022-01-27 14:30:03 +00:00
|
|
|
index++
|
|
|
|
err := srv.State().UpsertAllocs(structs.MsgTypeTestSetup, index,
|
|
|
|
[]*structs.Allocation{alloc})
|
2022-04-01 19:17:58 +00:00
|
|
|
require.NoError(t, err)
|
2022-01-27 14:30:03 +00:00
|
|
|
|
2022-01-24 16:49:50 +00:00
|
|
|
watcher.SetEnabled(true, srv.State(), "")
|
2020-04-30 13:13:00 +00:00
|
|
|
|
|
|
|
index++
|
2022-03-07 16:06:59 +00:00
|
|
|
err = srv.State().UpsertCSIVolume(index, []*structs.CSIVolume{vol})
|
2022-04-01 19:17:58 +00:00
|
|
|
require.NoError(t, err)
|
2020-04-30 13:13:00 +00:00
|
|
|
|
|
|
|
// we should get or start up a watcher when we get an update for
|
|
|
|
// the volume from the state store
|
2022-04-01 19:17:58 +00:00
|
|
|
require.Eventually(t, func() bool {
|
2021-06-14 10:11:35 +00:00
|
|
|
watcher.wlock.RLock()
|
|
|
|
defer watcher.wlock.RUnlock()
|
2020-04-30 13:13:00 +00:00
|
|
|
return 1 == len(watcher.watchers)
|
|
|
|
}, time.Second, 10*time.Millisecond)
|
|
|
|
|
2022-01-05 16:40:20 +00:00
|
|
|
vol, _ = srv.State().CSIVolumeByID(nil, vol.Namespace, vol.ID)
|
2022-04-01 19:17:58 +00:00
|
|
|
require.Len(t, vol.PastClaims, 0, "expected to have 0 PastClaims")
|
|
|
|
require.Equal(t, srv.countCSIUnpublish, 0, "expected no CSI.Unpublish RPC calls")
|
2022-01-05 16:40:20 +00:00
|
|
|
|
|
|
|
// trying to test a dropped watch is racy, so to reliably simulate
|
|
|
|
// this condition, step-down the watcher first and then perform
|
|
|
|
// the writes to the volume before starting the new watcher. no
|
|
|
|
// watches for that change will fire on the new watcher
|
|
|
|
|
|
|
|
// step-down (this is sync)
|
2022-01-24 16:49:50 +00:00
|
|
|
watcher.SetEnabled(false, nil, "")
|
2022-11-01 20:53:10 +00:00
|
|
|
watcher.wlock.RLock()
|
2022-04-01 19:17:58 +00:00
|
|
|
require.Equal(t, 0, len(watcher.watchers))
|
2022-11-01 20:53:10 +00:00
|
|
|
watcher.wlock.RUnlock()
|
2020-04-30 13:13:00 +00:00
|
|
|
|
2022-01-05 16:40:20 +00:00
|
|
|
// allocation is now invalid
|
|
|
|
index++
|
2022-07-06 14:30:11 +00:00
|
|
|
err = srv.State().DeleteEval(index, []string{}, []string{alloc.ID}, false)
|
2022-04-01 19:17:58 +00:00
|
|
|
require.NoError(t, err)
|
2022-01-05 16:40:20 +00:00
|
|
|
|
|
|
|
// emit a GC so that we have a volume change that's dropped
|
|
|
|
claim := &structs.CSIVolumeClaim{
|
|
|
|
AllocationID: alloc.ID,
|
|
|
|
NodeID: node.ID,
|
|
|
|
Mode: structs.CSIVolumeClaimGC,
|
|
|
|
State: structs.CSIVolumeClaimStateUnpublishing,
|
|
|
|
}
|
|
|
|
index++
|
|
|
|
err = srv.State().CSIVolumeClaim(index, vol.Namespace, vol.ID, claim)
|
2022-04-01 19:17:58 +00:00
|
|
|
require.NoError(t, err)
|
2022-01-05 16:40:20 +00:00
|
|
|
|
|
|
|
// create a new watcher and enable it to simulate the leadership
|
|
|
|
// transition
|
|
|
|
watcher = NewVolumesWatcher(testlog.HCLogger(t), srv, "")
|
2022-04-04 14:46:45 +00:00
|
|
|
watcher.quiescentTimeout = 100 * time.Millisecond
|
2022-01-24 16:49:50 +00:00
|
|
|
watcher.SetEnabled(true, srv.State(), "")
|
2022-01-05 16:40:20 +00:00
|
|
|
|
2022-04-01 19:17:58 +00:00
|
|
|
require.Eventually(t, func() bool {
|
2021-06-14 10:11:35 +00:00
|
|
|
watcher.wlock.RLock()
|
|
|
|
defer watcher.wlock.RUnlock()
|
2022-11-01 20:53:10 +00:00
|
|
|
return 0 == len(watcher.watchers)
|
2020-04-30 13:13:00 +00:00
|
|
|
}, time.Second, 10*time.Millisecond)
|
2022-01-05 16:40:20 +00:00
|
|
|
|
|
|
|
vol, _ = srv.State().CSIVolumeByID(nil, vol.Namespace, vol.ID)
|
2022-04-01 19:17:58 +00:00
|
|
|
require.Len(t, vol.PastClaims, 1, "expected to have 1 PastClaim")
|
|
|
|
require.Equal(t, srv.countCSIUnpublish, 1, "expected CSI.Unpublish RPC to be called")
|
2020-04-30 13:13:00 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// TestVolumeWatch_StartStop tests the start and stop of the watcher when
|
|
|
|
// it receives notifcations and has completed its work
|
|
|
|
func TestVolumeWatch_StartStop(t *testing.T) {
|
2022-03-15 12:42:43 +00:00
|
|
|
ci.Parallel(t)
|
2020-04-30 13:13:00 +00:00
|
|
|
|
|
|
|
srv := &MockStatefulRPCServer{}
|
|
|
|
srv.state = state.TestStateStore(t)
|
|
|
|
index := uint64(100)
|
2020-08-07 19:37:27 +00:00
|
|
|
watcher := NewVolumesWatcher(testlog.HCLogger(t), srv, "")
|
2022-04-04 14:46:45 +00:00
|
|
|
watcher.quiescentTimeout = 100 * time.Millisecond
|
2020-04-30 13:13:00 +00:00
|
|
|
|
2022-01-24 16:49:50 +00:00
|
|
|
watcher.SetEnabled(true, srv.State(), "")
|
2022-04-01 19:17:58 +00:00
|
|
|
require.Equal(t, 0, len(watcher.watchers))
|
2020-04-30 13:13:00 +00:00
|
|
|
|
|
|
|
plugin := mock.CSIPlugin()
|
2020-08-06 18:31:18 +00:00
|
|
|
node := testNode(plugin, srv.State())
|
2020-05-11 13:32:05 +00:00
|
|
|
alloc1 := mock.Alloc()
|
|
|
|
alloc1.ClientStatus = structs.AllocClientStatusRunning
|
2020-04-30 13:13:00 +00:00
|
|
|
alloc2 := mock.Alloc()
|
2020-05-11 13:32:05 +00:00
|
|
|
alloc2.Job = alloc1.Job
|
2020-04-30 13:13:00 +00:00
|
|
|
alloc2.ClientStatus = structs.AllocClientStatusRunning
|
|
|
|
index++
|
2023-04-11 13:45:08 +00:00
|
|
|
err := srv.State().UpsertJob(structs.MsgTypeTestSetup, index, nil, alloc1.Job)
|
2022-04-01 19:17:58 +00:00
|
|
|
require.NoError(t, err)
|
2020-04-30 13:13:00 +00:00
|
|
|
index++
|
2020-10-19 13:30:15 +00:00
|
|
|
err = srv.State().UpsertAllocs(structs.MsgTypeTestSetup, index, []*structs.Allocation{alloc1, alloc2})
|
2022-04-01 19:17:58 +00:00
|
|
|
require.NoError(t, err)
|
2020-04-30 13:13:00 +00:00
|
|
|
|
2022-11-01 20:53:10 +00:00
|
|
|
// register a volume and an unused volume
|
2020-08-06 18:31:18 +00:00
|
|
|
vol := testVolume(plugin, alloc1, node.ID)
|
2020-04-30 13:13:00 +00:00
|
|
|
index++
|
2022-03-07 16:06:59 +00:00
|
|
|
err = srv.State().UpsertCSIVolume(index, []*structs.CSIVolume{vol})
|
2022-04-01 19:17:58 +00:00
|
|
|
require.NoError(t, err)
|
2020-04-30 13:13:00 +00:00
|
|
|
|
2020-05-11 13:32:05 +00:00
|
|
|
// assert we get a watcher; there are no claims so it should immediately stop
|
2022-04-01 19:17:58 +00:00
|
|
|
require.Eventually(t, func() bool {
|
2021-06-14 10:11:35 +00:00
|
|
|
watcher.wlock.RLock()
|
|
|
|
defer watcher.wlock.RUnlock()
|
2022-11-01 20:53:10 +00:00
|
|
|
return 0 == len(watcher.watchers)
|
2020-05-11 13:32:05 +00:00
|
|
|
}, time.Second*2, 10*time.Millisecond)
|
2020-04-30 13:13:00 +00:00
|
|
|
|
|
|
|
// claim the volume for both allocs
|
|
|
|
claim := &structs.CSIVolumeClaim{
|
2020-05-11 13:32:05 +00:00
|
|
|
AllocationID: alloc1.ID,
|
2020-04-30 13:13:00 +00:00
|
|
|
NodeID: node.ID,
|
|
|
|
Mode: structs.CSIVolumeClaimRead,
|
2022-02-23 14:51:20 +00:00
|
|
|
AccessMode: structs.CSIVolumeAccessModeMultiNodeReader,
|
2020-04-30 13:13:00 +00:00
|
|
|
}
|
2022-02-23 14:51:20 +00:00
|
|
|
|
2020-04-30 13:13:00 +00:00
|
|
|
index++
|
|
|
|
err = srv.State().CSIVolumeClaim(index, vol.Namespace, vol.ID, claim)
|
2022-04-01 19:17:58 +00:00
|
|
|
require.NoError(t, err)
|
2020-04-30 13:13:00 +00:00
|
|
|
claim.AllocationID = alloc2.ID
|
|
|
|
index++
|
|
|
|
err = srv.State().CSIVolumeClaim(index, vol.Namespace, vol.ID, claim)
|
2022-04-01 19:17:58 +00:00
|
|
|
require.NoError(t, err)
|
2020-04-30 13:13:00 +00:00
|
|
|
|
|
|
|
// reap the volume and assert nothing has happened
|
|
|
|
claim = &structs.CSIVolumeClaim{
|
2020-05-11 13:32:05 +00:00
|
|
|
AllocationID: alloc1.ID,
|
2020-04-30 13:13:00 +00:00
|
|
|
NodeID: node.ID,
|
|
|
|
}
|
|
|
|
index++
|
|
|
|
err = srv.State().CSIVolumeClaim(index, vol.Namespace, vol.ID, claim)
|
2022-04-01 19:17:58 +00:00
|
|
|
require.NoError(t, err)
|
2020-05-11 13:32:05 +00:00
|
|
|
|
|
|
|
ws := memdb.NewWatchSet()
|
|
|
|
vol, _ = srv.State().CSIVolumeByID(ws, vol.Namespace, vol.ID)
|
2022-04-01 19:17:58 +00:00
|
|
|
require.Equal(t, 2, len(vol.ReadAllocs))
|
2020-04-30 13:13:00 +00:00
|
|
|
|
|
|
|
// alloc becomes terminal
|
2022-11-01 20:53:10 +00:00
|
|
|
alloc1 = alloc1.Copy()
|
2020-05-11 13:32:05 +00:00
|
|
|
alloc1.ClientStatus = structs.AllocClientStatusComplete
|
2020-04-30 13:13:00 +00:00
|
|
|
index++
|
2020-10-19 13:30:15 +00:00
|
|
|
err = srv.State().UpsertAllocs(structs.MsgTypeTestSetup, index, []*structs.Allocation{alloc1})
|
2022-04-01 19:17:58 +00:00
|
|
|
require.NoError(t, err)
|
2020-04-30 13:13:00 +00:00
|
|
|
index++
|
2020-04-30 21:11:31 +00:00
|
|
|
claim.State = structs.CSIVolumeClaimStateReadyToFree
|
2020-04-30 13:13:00 +00:00
|
|
|
err = srv.State().CSIVolumeClaim(index, vol.Namespace, vol.ID, claim)
|
2022-04-01 19:17:58 +00:00
|
|
|
require.NoError(t, err)
|
2020-04-30 13:13:00 +00:00
|
|
|
|
2022-11-01 20:53:10 +00:00
|
|
|
// watcher stops and 1 claim has been released
|
2022-04-01 19:17:58 +00:00
|
|
|
require.Eventually(t, func() bool {
|
2022-11-01 20:53:10 +00:00
|
|
|
watcher.wlock.RLock()
|
|
|
|
defer watcher.wlock.RUnlock()
|
|
|
|
return 0 == len(watcher.watchers)
|
|
|
|
}, time.Second*5, 10*time.Millisecond)
|
|
|
|
|
|
|
|
vol, _ = srv.State().CSIVolumeByID(ws, vol.Namespace, vol.ID)
|
|
|
|
must.Eq(t, 1, len(vol.ReadAllocs))
|
|
|
|
must.Eq(t, 0, len(vol.PastClaims))
|
|
|
|
}
|
|
|
|
|
|
|
|
// TestVolumeWatch_Delete tests the stop of the watcher when it receives
|
|
|
|
// notifications around a deleted volume
|
|
|
|
func TestVolumeWatch_Delete(t *testing.T) {
|
|
|
|
ci.Parallel(t)
|
|
|
|
|
|
|
|
srv := &MockStatefulRPCServer{}
|
|
|
|
srv.state = state.TestStateStore(t)
|
|
|
|
index := uint64(100)
|
|
|
|
watcher := NewVolumesWatcher(testlog.HCLogger(t), srv, "")
|
|
|
|
watcher.quiescentTimeout = 100 * time.Millisecond
|
|
|
|
|
|
|
|
watcher.SetEnabled(true, srv.State(), "")
|
|
|
|
must.Eq(t, 0, len(watcher.watchers))
|
|
|
|
|
|
|
|
// register an unused volume
|
|
|
|
plugin := mock.CSIPlugin()
|
|
|
|
vol := mock.CSIVolume(plugin)
|
|
|
|
index++
|
|
|
|
must.NoError(t, srv.State().UpsertCSIVolume(index, []*structs.CSIVolume{vol}))
|
|
|
|
|
|
|
|
// assert we get a watcher; there are no claims so it should immediately stop
|
|
|
|
require.Eventually(t, func() bool {
|
|
|
|
watcher.wlock.RLock()
|
|
|
|
defer watcher.wlock.RUnlock()
|
|
|
|
return 0 == len(watcher.watchers)
|
2020-04-30 13:13:00 +00:00
|
|
|
}, time.Second*2, 10*time.Millisecond)
|
|
|
|
|
2022-11-01 20:53:10 +00:00
|
|
|
// write a GC claim to the volume and then immediately delete, to
|
|
|
|
// potentially hit the race condition between updates and deletes
|
|
|
|
index++
|
|
|
|
must.NoError(t, srv.State().CSIVolumeClaim(index, vol.Namespace, vol.ID,
|
|
|
|
&structs.CSIVolumeClaim{
|
|
|
|
Mode: structs.CSIVolumeClaimGC,
|
|
|
|
State: structs.CSIVolumeClaimStateReadyToFree,
|
|
|
|
}))
|
|
|
|
|
|
|
|
index++
|
|
|
|
must.NoError(t, srv.State().CSIVolumeDeregister(
|
|
|
|
index, vol.Namespace, []string{vol.ID}, false))
|
|
|
|
|
|
|
|
// the watcher should not be running
|
2022-04-01 19:17:58 +00:00
|
|
|
require.Eventually(t, func() bool {
|
2021-06-14 10:11:35 +00:00
|
|
|
watcher.wlock.RLock()
|
|
|
|
defer watcher.wlock.RUnlock()
|
2022-11-01 20:53:10 +00:00
|
|
|
return 0 == len(watcher.watchers)
|
2020-05-11 13:32:05 +00:00
|
|
|
}, time.Second*5, 10*time.Millisecond)
|
2022-11-01 20:53:10 +00:00
|
|
|
|
2020-04-30 13:13:00 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// TestVolumeWatch_RegisterDeregister tests the start and stop of
|
|
|
|
// watchers around registration
|
|
|
|
func TestVolumeWatch_RegisterDeregister(t *testing.T) {
|
2022-03-15 12:42:43 +00:00
|
|
|
ci.Parallel(t)
|
2020-04-30 13:13:00 +00:00
|
|
|
|
|
|
|
srv := &MockStatefulRPCServer{}
|
|
|
|
srv.state = state.TestStateStore(t)
|
|
|
|
|
|
|
|
index := uint64(100)
|
|
|
|
|
2020-08-07 19:37:27 +00:00
|
|
|
watcher := NewVolumesWatcher(testlog.HCLogger(t), srv, "")
|
2022-04-04 14:46:45 +00:00
|
|
|
watcher.quiescentTimeout = 10 * time.Millisecond
|
2020-04-30 13:13:00 +00:00
|
|
|
|
2022-01-24 16:49:50 +00:00
|
|
|
watcher.SetEnabled(true, srv.State(), "")
|
2022-04-01 19:17:58 +00:00
|
|
|
require.Equal(t, 0, len(watcher.watchers))
|
2020-04-30 13:13:00 +00:00
|
|
|
|
|
|
|
plugin := mock.CSIPlugin()
|
|
|
|
alloc := mock.Alloc()
|
|
|
|
alloc.ClientStatus = structs.AllocClientStatusComplete
|
|
|
|
|
2020-08-06 18:31:18 +00:00
|
|
|
// register a volume without claims
|
|
|
|
vol := mock.CSIVolume(plugin)
|
2020-04-30 13:13:00 +00:00
|
|
|
index++
|
2022-03-07 16:06:59 +00:00
|
|
|
err := srv.State().UpsertCSIVolume(index, []*structs.CSIVolume{vol})
|
2022-04-01 19:17:58 +00:00
|
|
|
require.NoError(t, err)
|
2020-04-30 13:13:00 +00:00
|
|
|
|
2022-11-01 20:53:10 +00:00
|
|
|
// watcher should stop
|
2022-04-01 19:17:58 +00:00
|
|
|
require.Eventually(t, func() bool {
|
2021-06-14 10:11:35 +00:00
|
|
|
watcher.wlock.RLock()
|
|
|
|
defer watcher.wlock.RUnlock()
|
2022-11-01 20:53:10 +00:00
|
|
|
return 0 == len(watcher.watchers)
|
2020-04-30 13:13:00 +00:00
|
|
|
}, time.Second, 10*time.Millisecond)
|
|
|
|
}
|