From 68e7efa128010e8faa28c5fa1def11d9af79d721 Mon Sep 17 00:00:00 2001 From: Lena Garber <114949949+lgarber-akamai@users.noreply.github.com> Date: Tue, 12 Dec 2023 11:30:34 -0500 Subject: [PATCH] ref: Refactor watcher system to use genericWatcher; improve project documentation (#20) * Refactor watcher system to use genericWatcher; improve docs * oops * Add examples to package docs --- README.md | 2 + doc.go | 36 ++++++++ examples/README.md | 6 ++ examples/instancewatcher/README.md | 11 +++ examples/instancewatcher/main.go | 39 ++++++++ examples/networkwatcher/README.md | 11 +++ examples/networkwatcher/main.go | 39 ++++++++ test/integration/watcher_test.go | 4 +- watcher.go | 143 ----------------------------- watcher_generic.go | 104 +++++++++++++++++++++ watchers.go | 81 ++++++++++++++++ 11 files changed, 331 insertions(+), 145 deletions(-) create mode 100644 doc.go create mode 100644 examples/README.md create mode 100644 examples/instancewatcher/README.md create mode 100644 examples/instancewatcher/main.go create mode 100644 examples/networkwatcher/README.md create mode 100644 examples/networkwatcher/main.go delete mode 100644 watcher.go create mode 100644 watcher_generic.go create mode 100644 watchers.go diff --git a/README.md b/README.md index 3dcce66..18ccbb9 100644 --- a/README.md +++ b/README.md @@ -92,6 +92,8 @@ func main() { } ``` +For more examples, visit the [examples directory](./examples). + ## Documentation See [godoc](https://pkg.go.dev/github.com/linode/go-metadata) for a complete documentation reference. diff --git a/doc.go b/doc.go new file mode 100644 index 0000000..610f8e2 --- /dev/null +++ b/doc.go @@ -0,0 +1,36 @@ +/* +go-metadata allows Go projects to easily interact with the Linode Metadata Service. + +Basic example: + package main + + import ( + "context" + "fmt" + "log" + + metadata "github.com/linode/go-metadata" + ) + + func main() { + // Create a new client + client, err := metadata.NewClient(context.Background()) + if err != nil { + log.Fatal(err) + } + + // Retrieve metadata about the current instance from the metadata API + instanceInfo, err := client.GetInstance(context.Background()) + if err != nil { + log.Fatal(err) + } + + fmt.Println("Instance Label:", instanceInfo.Label) + } + +For more examples see: https://github.com/linode/go-metadata/tree/ref/watcher-refactor/examples + +To learn more about the Linode Metadata Service, see the official guide: https://www.linode.com/docs/products/compute/compute-instances/guides/metadata/?tabs=linode-api +*/ + +package metadata diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..500ebe7 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,6 @@ +# go-metadata Examples + +This directory contains various runnable examples for using this package. + +- [Instance Watcher](./instancewatcher) - Watch for changes to the current Linode instance. +- [Network Watcher](./networkwatcher) - Watch for changes to the current Linode instance's networking. \ No newline at end of file diff --git a/examples/instancewatcher/README.md b/examples/instancewatcher/README.md new file mode 100644 index 0000000..513b114 --- /dev/null +++ b/examples/instancewatcher/README.md @@ -0,0 +1,11 @@ +# Instance Watcher Example + +This example makes use of the InstanceWatcher struct to watch for changes to the current Linode instance. + +### Running this Example + +NOTE: This example must be run from within a Linode instance. + +```bash +go run main.go +``` \ No newline at end of file diff --git a/examples/instancewatcher/main.go b/examples/instancewatcher/main.go new file mode 100644 index 0000000..a0ff329 --- /dev/null +++ b/examples/instancewatcher/main.go @@ -0,0 +1,39 @@ +package main + +import ( + "context" + metadata "github.com/linode/go-metadata" + "log" + "time" +) + +func main() { + ctx := context.Background() + + // Create a new client + client, err := metadata.NewClient(ctx) + if err != nil { + log.Fatal(err) + } + + // Create a new instance watcher + instanceWatcher := client.NewInstanceWatcher( + metadata.WatcherWithInterval(10 * time.Second), + ) + + // Start the network watcher in a goroutine. + go instanceWatcher.Start(ctx) + + // Wait for changes + for { + select { + case data := <-instanceWatcher.Updates: + log.Printf( + "Change to instance detected.\nNew data: %v\n", + data, + ) + case err := <-instanceWatcher.Errors: + log.Fatalf("Got error from instance watcher: %s", err) + } + } +} diff --git a/examples/networkwatcher/README.md b/examples/networkwatcher/README.md new file mode 100644 index 0000000..8edcfd1 --- /dev/null +++ b/examples/networkwatcher/README.md @@ -0,0 +1,11 @@ +# Network Watcher Example + +This example makes use of the NetworkWatcher struct to watch for changes to the current Linode instance's networking. + +### Running this Example + +NOTE: This example must be run from within a Linode instance. + +```bash +go run main.go +``` \ No newline at end of file diff --git a/examples/networkwatcher/main.go b/examples/networkwatcher/main.go new file mode 100644 index 0000000..ccb7613 --- /dev/null +++ b/examples/networkwatcher/main.go @@ -0,0 +1,39 @@ +package main + +import ( + "context" + metadata "github.com/linode/go-metadata" + "log" + "time" +) + +func main() { + ctx := context.Background() + + // Create a new client + client, err := metadata.NewClient(ctx) + if err != nil { + log.Fatal(err) + } + + // Create a new network watcher + networkWatcher := client.NewNetworkWatcher( + metadata.WatcherWithInterval(10 * time.Second), + ) + + // Start the network watcher in a goroutine. + go networkWatcher.Start(ctx) + + // Wait for changes + for { + select { + case data := <-networkWatcher.Updates: + log.Printf( + "Change to network configuration detected.\nNew data: %v\n", + data, + ) + case err := <-networkWatcher.Errors: + log.Fatalf("Got error from network watcher: %s", err) + } + } +} diff --git a/test/integration/watcher_test.go b/test/integration/watcher_test.go index 755879a..526ce7d 100644 --- a/test/integration/watcher_test.go +++ b/test/integration/watcher_test.go @@ -56,7 +56,7 @@ func TestNetworkWatcher(t *testing.T) { assert.NoError(t, err) watcher := metadataClient.NewNetworkWatcher(metadata.WatcherWithInterval(1 * time.Second)) - watcher.Start(ctx) + go watcher.Start(ctx) numUpdates := 0 for i := 1; i <= 5; i++ { updateData := <-watcher.Updates @@ -116,7 +116,7 @@ func TestInstanceWatcher(t *testing.T) { assert.NoError(t, err) watcher := metadataClient.NewInstanceWatcher(metadata.WatcherWithInterval(1 * time.Second)) - watcher.Start(ctx) + go watcher.Start(ctx) numUpdates := 0 for i := 1; i <= 5; i++ { updateData := <-watcher.Updates diff --git a/watcher.go b/watcher.go deleted file mode 100644 index 2c7d18a..0000000 --- a/watcher.go +++ /dev/null @@ -1,143 +0,0 @@ -package metadata - -import ( - "context" - "reflect" - "time" -) - -const DefaultWatcherInterval = 5 * time.Minute - -type NetworkWatcher struct { - Updates chan *NetworkData - Errors chan error - cancel chan struct{} - client *Client - interval time.Duration - ticker *time.Ticker -} - -func (watcher *NetworkWatcher) Start(ctx context.Context) { - go func() { - var oldNetworkData *NetworkData - watcher.ticker = time.NewTicker(watcher.interval) - defer watcher.ticker.Stop() - - for { - select { - case <-ctx.Done(): - return - case <-watcher.cancel: - return - case <-watcher.ticker.C: - networkData, err := watcher.client.GetNetwork(ctx) - if err != nil { - watcher.Errors <- err - } - if !reflect.DeepEqual(networkData, oldNetworkData) { - watcher.Updates <- networkData - oldNetworkData = networkData - } - } - } - }() -} - -func (watcher *NetworkWatcher) Close() error { - close(watcher.cancel) - close(watcher.Errors) - close(watcher.Updates) - watcher.ticker.Stop() - return nil -} - -type InstanceWatcher struct { - Updates chan *InstanceData - Errors chan error - cancel chan struct{} - client *Client - interval time.Duration - ticker *time.Ticker -} - -func (watcher *InstanceWatcher) Start(ctx context.Context) { - go func() { - var oldInstanceData *InstanceData - watcher.ticker = time.NewTicker(watcher.interval) - defer watcher.ticker.Stop() - - for { - select { - case <-ctx.Done(): - return - case <-watcher.cancel: - return - case <-watcher.ticker.C: - instanceData, err := watcher.client.GetInstance(ctx) - if err != nil { - watcher.Errors <- err - } - if !reflect.DeepEqual(instanceData, oldInstanceData) { // Todo Testing - watcher.Updates <- instanceData - oldInstanceData = instanceData - } - } - } - }() -} - -func (watcher *InstanceWatcher) Close() error { - close(watcher.cancel) - close(watcher.Errors) - close(watcher.Updates) - watcher.ticker.Stop() - return nil -} - -type WatcherOption func(options *watcherConfig) - -type watcherConfig struct { - Interval time.Duration -} - -func (c *Client) NewInstanceWatcher(opts ...WatcherOption) *InstanceWatcher { - watcherOpts := watcherConfig{ - Interval: DefaultWatcherInterval, - } - - for _, opt := range opts { - opt(&watcherOpts) - } - - return &InstanceWatcher{ - Updates: make(chan *InstanceData), - Errors: make(chan error), - cancel: make(chan struct{}), - interval: watcherOpts.Interval, - client: c, - } -} - -func (c *Client) NewNetworkWatcher(opts ...WatcherOption) *NetworkWatcher { - watcherOpts := watcherConfig{ - Interval: DefaultWatcherInterval, - } - - for _, opt := range opts { - opt(&watcherOpts) - } - - return &NetworkWatcher{ - Updates: make(chan *NetworkData), - Errors: make(chan error), - cancel: make(chan struct{}), - interval: watcherOpts.Interval, - client: c, - } -} - -func WatcherWithInterval(duration time.Duration) WatcherOption { - return func(options *watcherConfig) { - options.Interval = duration - } -} diff --git a/watcher_generic.go b/watcher_generic.go new file mode 100644 index 0000000..a0dcb7f --- /dev/null +++ b/watcher_generic.go @@ -0,0 +1,104 @@ +package metadata + +import ( + "context" + "reflect" + "time" +) + +const DefaultWatcherInterval = 5 * time.Minute + +// WatcherOption represents an option that can be used to configure +// a watcher. +type WatcherOption func(options *watcherConfig) + +type watcherConfig struct { + Interval time.Duration +} + +type resourceFetchFunc[T any] func(ctx context.Context) (T, error) + +// genericWatcher is a resource-agnostic watcher that allows +// us to re-use polling logic across various watcher implementations. +type genericWatcher[T any] struct { + Updates chan T + Errors chan error + cancel chan bool + + fetchResource resourceFetchFunc[T] + + interval time.Duration +} + +// Start starts the watcher. +func (watcher *genericWatcher[T]) Start(ctx context.Context) { + var oldData T + + ticker := time.NewTicker(watcher.interval) + defer ticker.Stop() + + defer func() { + close(watcher.Updates) + close(watcher.Errors) + close(watcher.cancel) + }() + + for { + select { + case <-ctx.Done(): + return + case <-watcher.cancel: + return + case <-ticker.C: + data, err := watcher.fetchResource(ctx) + if err != nil { + watcher.Errors <- err + } + + if !reflect.DeepEqual(data, oldData) { + watcher.Updates <- data + oldData = data + } + } + } +} + +// Close closes the watcher and all related channels. +func (watcher *genericWatcher[T]) Close() { + // Send a signal to cancel the poller. + // All channels wil be implicitly cleaned up in + // the start goroutine, else they will be + // cleaned up by the garbage collector. + watcher.cancel <- true +} + +// newGenericWatcher creates a new instance of an endpoint-agnostic watcher. +func newGenericWatcher[T any]( + fetchResource resourceFetchFunc[T], + opts ...WatcherOption, +) *genericWatcher[T] { + watcherOpts := watcherConfig{ + Interval: DefaultWatcherInterval, + } + + for _, opt := range opts { + opt(&watcherOpts) + } + + return &genericWatcher[T]{ + Updates: make(chan T), + Errors: make(chan error), + cancel: make(chan bool, 1), + interval: watcherOpts.Interval, + fetchResource: fetchResource, + } +} + +// WatcherWithInterval configures the interval at which +// a watcher should poll for changes. +// Default: 5 minutes +func WatcherWithInterval(duration time.Duration) WatcherOption { + return func(options *watcherConfig) { + options.Interval = duration + } +} diff --git a/watchers.go b/watchers.go new file mode 100644 index 0000000..0a516bb --- /dev/null +++ b/watchers.go @@ -0,0 +1,81 @@ +package metadata + +import ( + "context" +) + +// InstanceWatcher watches for any changes that are reflected +// in the Client.GetInstance(...) function result. +type InstanceWatcher struct { + Updates chan *InstanceData + Errors chan error + + watcher *genericWatcher[*InstanceData] +} + +// Start starts the watcher. +// NOTE: Start should only be called once per-watcher. +func (watcher *InstanceWatcher) Start(ctx context.Context) { + watcher.watcher.Start(ctx) +} + +// Close closes the watcher and all related channels. +// If applicable, close will also cancel the poller for this watcher. +func (watcher *InstanceWatcher) Close() { + watcher.watcher.Close() +} + +// NewInstanceWatcher creates a new InstanceWatcher for monitoring +// changes to the current Linode instance. +func (c *Client) NewInstanceWatcher(opts ...WatcherOption) *InstanceWatcher { + coreWatcher := newGenericWatcher( + func(ctx context.Context) (*InstanceData, error) { + return c.GetInstance(ctx) + }, + opts..., + ) + + return &InstanceWatcher{ + Updates: coreWatcher.Updates, + Errors: coreWatcher.Errors, + watcher: coreWatcher, + } +} + +// NetworkWatcher watches for any changes that are reflected +// in the Client.GetNetwork(...) function result. +type NetworkWatcher struct { + Updates chan *NetworkData + Errors chan error + + watcher *genericWatcher[*NetworkData] +} + +// Start starts the watcher. +// NOTE: Start should only be called once per-watcher. +func (watcher *NetworkWatcher) Start(ctx context.Context) { + watcher.watcher.Start(ctx) +} + +// Close closes the watcher and all related channels. +// If applicable, close will also cancel the poller for this watcher. +func (watcher *NetworkWatcher) Close() { + watcher.watcher.Close() +} + +// NewNetworkWatcher creates a new NetworkWatcher for monitoring +// changes to the current Linode instance. +func (c *Client) NewNetworkWatcher(opts ...WatcherOption) *NetworkWatcher { + coreWatcher := newGenericWatcher( + func(ctx context.Context) (*NetworkData, error) { + return c.GetNetwork(ctx) + }, + opts..., + ) + + return &NetworkWatcher{ + Updates: coreWatcher.Updates, + Errors: coreWatcher.Errors, + watcher: coreWatcher, + } +}