github.com/juju/juju@v0.0.0-20240327075706-a90865de2538/worker/lease/util_test.go (about)

     1  // Copyright 2015 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package lease_test
     5  
     6  import (
     7  	"context"
     8  	"sync"
     9  	"time"
    10  
    11  	"github.com/juju/clock/testclock"
    12  	"github.com/juju/errors"
    13  	"github.com/juju/names/v5"
    14  	jc "github.com/juju/testing/checkers"
    15  	gc "gopkg.in/check.v1"
    16  
    17  	"github.com/juju/juju/core/lease"
    18  	coretesting "github.com/juju/juju/testing"
    19  )
    20  
    21  // Secretary implements lease.Secretary for testing purposes.
    22  type Secretary struct{}
    23  
    24  // CheckLease is part of the lease.Secretary interface.
    25  func (Secretary) CheckLease(key lease.Key) error {
    26  	return checkName(key.Lease)
    27  }
    28  
    29  // CheckHolder is part of the lease.Secretary interface.
    30  func (Secretary) CheckHolder(name string) error {
    31  	return checkName(name)
    32  }
    33  
    34  func checkName(name string) error {
    35  	if name == "INVALID" {
    36  		return errors.NotValidf("name")
    37  	}
    38  	return nil
    39  }
    40  
    41  // CheckDuration is part of the lease.Secretary interface.
    42  func (Secretary) CheckDuration(duration time.Duration) error {
    43  	if duration != time.Minute {
    44  		return errors.NotValidf("time")
    45  	}
    46  	return nil
    47  }
    48  
    49  // Store implements corelease.Store for testing purposes.
    50  type Store struct {
    51  	mu           sync.Mutex
    52  	clock        *testclock.Clock
    53  	leases       map[lease.Key]lease.Info
    54  	expect       []call
    55  	failed       chan error
    56  	runningCalls int
    57  	done         chan struct{}
    58  }
    59  
    60  // NewStore initializes and returns a new store configured to report
    61  // the supplied leases and expect the supplied calls.
    62  func NewStore(leases map[lease.Key]lease.Info, expect []call, clock *testclock.Clock) *Store {
    63  	if leases == nil {
    64  		leases = make(map[lease.Key]lease.Info)
    65  	}
    66  	done := make(chan struct{})
    67  	if len(expect) == 0 {
    68  		close(done)
    69  	}
    70  	return &Store{
    71  		leases: leases,
    72  		expect: expect,
    73  		done:   done,
    74  		failed: make(chan error, 1000),
    75  		clock:  clock,
    76  	}
    77  }
    78  
    79  // Wait will return when all expected calls have been made, or fail the test
    80  // if they don't happen within a second. (You control the clock; your tests
    81  // should pass in *way* less than 10 seconds of wall-clock time.)
    82  func (store *Store) Wait(c *gc.C) {
    83  	select {
    84  	case <-store.done:
    85  		select {
    86  		case err := <-store.failed:
    87  			c.Errorf(err.Error())
    88  		default:
    89  		}
    90  	case <-time.After(coretesting.LongWait):
    91  		store.mu.Lock()
    92  		remaining := make([]string, len(store.expect))
    93  		for i := range store.expect {
    94  			remaining[i] = store.expect[i].method
    95  		}
    96  		store.mu.Unlock()
    97  		c.Errorf("Store test took way too long, still expecting %v", remaining)
    98  	}
    99  }
   100  
   101  func (store *Store) expireLeases() {
   102  	store.mu.Lock()
   103  	defer store.mu.Unlock()
   104  	for k, v := range store.leases {
   105  		if store.clock.Now().Before(v.Expiry) {
   106  			continue
   107  		}
   108  		delete(store.leases, k)
   109  	}
   110  }
   111  
   112  // Leases is part of the lease.Store interface.
   113  func (store *Store) Leases(_ context.Context, keys ...lease.Key) (map[lease.Key]lease.Info, error) {
   114  	filter := make(map[lease.Key]bool)
   115  	filtering := len(keys) > 0
   116  	if filtering {
   117  		for _, key := range keys {
   118  			filter[key] = true
   119  		}
   120  	}
   121  
   122  	store.mu.Lock()
   123  	defer store.mu.Unlock()
   124  	result := make(map[lease.Key]lease.Info)
   125  	for k, v := range store.leases {
   126  		if filtering && !filter[k] {
   127  			continue
   128  		}
   129  		result[k] = v
   130  	}
   131  	return result, nil
   132  }
   133  
   134  // LeaseGroup is part of the lease.Store interface.
   135  func (store *Store) LeaseGroup(ctx context.Context, namespace, modelUUID string) (map[lease.Key]lease.Info, error) {
   136  	leases, err := store.Leases(ctx)
   137  	if err != nil {
   138  		return nil, err
   139  	}
   140  
   141  	results := make(map[lease.Key]lease.Info)
   142  	for key, info := range leases {
   143  		if key.Namespace == namespace && key.ModelUUID == modelUUID {
   144  			results[key] = info
   145  		}
   146  	}
   147  	return results, nil
   148  }
   149  
   150  func (store *Store) closeIfEmpty() {
   151  	// This must be called with the lock held.
   152  	if store.runningCalls > 1 {
   153  		// The last one to leave should turn out the lights.
   154  		return
   155  	}
   156  	if len(store.expect) == 0 || len(store.failed) > 0 {
   157  		close(store.done)
   158  	}
   159  }
   160  
   161  // call implements the bulk of the lease.Store interface.
   162  func (store *Store) call(method string, args []interface{}) error {
   163  	store.mu.Lock()
   164  	defer store.mu.Unlock()
   165  
   166  	store.runningCalls++
   167  	defer func() {
   168  		store.runningCalls--
   169  	}()
   170  
   171  	select {
   172  	case <-store.done:
   173  		err := errors.Errorf("Store method called after test complete: %s %v", method, args)
   174  		store.failed <- err
   175  		return err
   176  	default:
   177  	}
   178  	defer store.closeIfEmpty()
   179  
   180  	if len(store.expect) < 1 {
   181  		err := errors.Errorf("store.%s called but was not expected", method)
   182  		store.failed <- err
   183  		return err
   184  	}
   185  	expect := store.expect[0]
   186  	store.expect = store.expect[1:]
   187  	if expect.parallelCallback != nil {
   188  		store.mu.Unlock()
   189  		expect.parallelCallback(&store.mu, store.leases)
   190  		store.mu.Lock()
   191  	}
   192  	if expect.callback != nil {
   193  		expect.callback(store.leases)
   194  	}
   195  
   196  	if method == expect.method {
   197  		if ok, _ := jc.DeepEqual(args, expect.args); ok {
   198  			return expect.err
   199  		}
   200  	}
   201  	err := errors.Errorf("unexpected Store call:\n  actual: %s %v\n  expect: %s %v",
   202  		method, args, expect.method, expect.args,
   203  	)
   204  	store.failed <- err
   205  	return err
   206  }
   207  
   208  // ClaimLease is part of the corelease.Store interface.
   209  func (store *Store) ClaimLease(_ context.Context, key lease.Key, request lease.Request) error {
   210  	return store.call("ClaimLease", []interface{}{key, request})
   211  }
   212  
   213  // ExtendLease is part of the corelease.Store interface.
   214  func (store *Store) ExtendLease(_ context.Context, key lease.Key, request lease.Request) error {
   215  	return store.call("ExtendLease", []interface{}{key, request})
   216  }
   217  
   218  func (store *Store) RevokeLease(_ context.Context, lease lease.Key, holder string) error {
   219  	return store.call("RevokeLease", []interface{}{lease, holder})
   220  }
   221  
   222  // PinLease is part of the corelease.Store interface.
   223  func (store *Store) PinLease(_ context.Context, key lease.Key, entity string) error {
   224  	return store.call("PinLease", []interface{}{key, entity})
   225  }
   226  
   227  // UnpinLease is part of the corelease.Store interface.
   228  func (store *Store) UnpinLease(_ context.Context, key lease.Key, entity string) error {
   229  	return store.call("UnpinLease", []interface{}{key, entity})
   230  }
   231  
   232  func (store *Store) Pinned(_ context.Context) (map[lease.Key][]string, error) {
   233  	_ = store.call("Pinned", nil)
   234  	return map[lease.Key][]string{
   235  		{
   236  			Namespace: "namespace",
   237  			ModelUUID: "modelUUID",
   238  			Lease:     "redis",
   239  		}: {names.NewMachineTag("0").String()},
   240  		{
   241  			Namespace: "ignored-namespace",
   242  			ModelUUID: "ignored modelUUID",
   243  			Lease:     "lolwut",
   244  		}: {names.NewMachineTag("666").String()},
   245  	}, nil
   246  }
   247  
   248  // call defines a expected method call on a Store; it encodes:
   249  type call struct {
   250  
   251  	// method is the name of the method.
   252  	method string
   253  
   254  	// args is the expected arguments.
   255  	args []interface{}
   256  
   257  	// err is the error to return.
   258  	err error
   259  
   260  	// callback, if non-nil, will be passed the internal leases dict; for
   261  	// modification, if desired. Otherwise you can use it to, e.g., assert
   262  	// clock time.
   263  	callback func(leases map[lease.Key]lease.Info)
   264  
   265  	// parallelCallback is like callback, but is also passed the
   266  	// lock. It's for testing calls that happen in parallel, where one
   267  	// might take longer than another. Any update to the leases dict
   268  	// must only happen while the lock is held.
   269  	parallelCallback func(mu *sync.Mutex, leases map[lease.Key]lease.Info)
   270  }
   271  
   272  func key(args ...string) lease.Key {
   273  	result := lease.Key{
   274  		Namespace: "namespace",
   275  		ModelUUID: "modelUUID",
   276  		Lease:     "lease",
   277  	}
   278  	if len(args) == 1 {
   279  		result.Lease = args[0]
   280  	} else if len(args) == 3 {
   281  		result.Namespace = args[0]
   282  		result.ModelUUID = args[1]
   283  		result.Lease = args[2]
   284  	}
   285  	return result
   286  }