Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Poller implementation periodically fetches workspace configs #2

Merged
merged 6 commits into from
Jan 4, 2024

Conversation

fxenik
Copy link
Member

@fxenik fxenik commented Jun 29, 2023

Description of the change

Implements background fetching of workspace configs. ControlPlane SDK now maintains a local workspace config cache, which is periodically updated by a background goroutine. More specifically:

Poller implementation

  • Starts a background goroutine that polls for workspace configs using a Client dependency.
  • modelv2.WorkspaceConfigs now exposes an UpdatedAt function that returns the maximum updated at timestamp of included configs.
  • Poller leverages this information to only fetch the full configuration on start and updates on every subsequent call.
  • Poller delegates processing of workspace configs to a WorkspaceConfigHandler function.

WorkspaceConfigCache

  • Provides Get & Set functions to read/write workspace configs.
  • Supports subscribers that get notified when configs are updated (through Set).
  • Get returns a copy of the workspace configs in order to avoid race conditions while subscribers read the result.
  • Set merges workspace config with previous cache contents, using the following rules:
    • merges source and destination definitions. Missing definitions in input have no effect, the rest are replaced.
    • missing workspaces are removed.
    • workspaces with nil configuration are ignored.
    • the rest of workspaces are being updated.
  • Set produces a notification to all Subscriber channels. Channel readers are guaranteed to get the latest configuration indirectly by using ControlPlane.GetWorkspaceConfigs().

ControlPlane

  • additional WithPollingInterval to control Poller's interval. If set to 0, polling is disabled.
  • GetWorkspaceConfigs now fetches config from cache if polling is enabled. If not, it makes an http call using Client.
  • supports config updates through Subscribe.

Note Poller and WorkspaceConfigCache have been implemented in different commits for easier review.

Notion link

Type of change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)

Related issues

Fix #1

Checklists

Development

  • Lint rules pass locally
  • The code changed/added as part of this pull request has been covered with tests
  • All tests related to the changed code pass in development

Code review

  • This pull request has a descriptive title and information useful to a reviewer. There may be a screenshot or screencast attached
  • "Ready for review" label attached to the PR and reviewers mentioned in a comment
  • Changes have been reviewed by at least one other engineer
  • Issue from task tracker has a link to this pull request

@fxenik fxenik force-pushed the initial-setup branch 2 times, most recently from 8013cf8 to 17465b6 Compare June 29, 2023 15:59
@fxenik fxenik force-pushed the poller branch 3 times, most recently from 87f77f5 to 8fa44bf Compare June 29, 2023 18:56
* Split workspace config model to multiple files
* Explicitly name model version in package
* Renamed SDK to Control Plane
* Refactored sample app for best practices
* Used url.URL to configure base URL for better error handling
* Split workspace config model to multiple files
* Explicitly name model version in package
* Renamed SDK to Control Plane
* Refactored sample app for best practices
* Used url.URL to configure base URL for better error handling
@fxenik fxenik changed the base branch from initial-setup to main August 1, 2023 10:17
@fxenik fxenik marked this pull request as ready for review January 2, 2024 13:54
@achettyiitr achettyiitr self-requested a review January 2, 2024 13:54

// Start starts the poller goroutine. It will poll for new workspace configs every interval.
// It will stop polling when the context is cancelled.
func (p *Poller) Start(ctx context.Context) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[minor] Although we have a way to stop this goroutine, there is no way to know when it got terminated or not. This could be an issue in some cases.

I would suggest on of the following:

  1. Make Start a blocking method, called Run(ctx context.Context)
  2. Introduce a background context, remove ctx from params and add a Stop method, that blocks until the go routine returns

}

if err := p.handler(response); err != nil {
return fmt.Errorf("failed to handle workspace configs: %w", err)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[minor] To make the final error message more readable avoid including words like error or failed when wrapping and returning messages, as they will be redundant when wrapped multiple times.

Suggested change
return fmt.Errorf("failed to handle workspace configs: %w", err)
return fmt.Errorf("handling workspace configs: %w", err)

p.log.Debug("polling for workspace configs")
wcs, err := p.client.GetWorkspaceConfigs(ctx)
if err != nil {
return fmt.Errorf("failed to get workspace configs: %w", err)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[minor]

Suggested change
return fmt.Errorf("failed to get workspace configs: %w", err)
return fmt.Errorf("get workspace configs: %w", err)

}

// remove deleted workspace configs (missing ids)
currentIds := make([]string, 0, len(c.configs.Workspaces))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit-pick] idiomatic way in go

Suggested change
currentIds := make([]string, 0, len(c.configs.Workspaces))
currentIDs := make([]string, 0, len(c.configs.Workspaces))

return nil
}

return copyConfigs(c.configs)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should consider the memory usage vs safety tread-offs of coping configs here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should consider writing an integration level test, that would combine multiple components, using httptest instead of mocks

Comment on lines +22 to +23
c.updateLock.Lock()
defer c.updateLock.Unlock()
Copy link
Member

@achettyiitr achettyiitr Jan 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can use read locks here, since we are just reading from the configs.

Suggested change
c.updateLock.Lock()
defer c.updateLock.Unlock()
c.updateLock.RLock()
defer c.updateLock.RUnlock()

Comment on lines +43 to +44
c.subscriberLock.Lock()
defer c.subscriberLock.Unlock()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can use read locks here, since we are just reading from the slice.

Suggested change
c.subscriberLock.Lock()
defer c.subscriberLock.Unlock()
c.subscriberLock.RLock()
defer c.subscriberLock.RUnlock()

Comment on lines +108 to +118
for k, v := range c.SourceDefinitions {
wc.SourceDefinitions[k] = v
}

for k, v := range c.DestinationDefinitions {
wc.DestinationDefinitions[k] = v
}

for k, v := range c.Workspaces {
wc.Workspaces[k] = v
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
for k, v := range c.SourceDefinitions {
wc.SourceDefinitions[k] = v
}
for k, v := range c.DestinationDefinitions {
wc.DestinationDefinitions[k] = v
}
for k, v := range c.Workspaces {
wc.Workspaces[k] = v
}
maps.Copy(wc.SourceDefinitions, c.SourceDefinitions)
maps.Copy(wc.DestinationDefinitions, c.DestinationDefinitions)
maps.Copy(wc.Workspaces, c.Workspaces)

Comment on lines +57 to +63
for id, config := range configs.SourceDefinitions {
c.configs.SourceDefinitions[id] = config
}

for id, config := range configs.DestinationDefinitions {
c.configs.DestinationDefinitions[id] = config
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
for id, config := range configs.SourceDefinitions {
c.configs.SourceDefinitions[id] = config
}
for id, config := range configs.DestinationDefinitions {
c.configs.DestinationDefinitions[id] = config
}
maps.Copy(c.configs.SourceDefinitions, configs.SourceDefinitions)
maps.Copy(c.configs.DestinationDefinitions, configs.DestinationDefinitions)

defer c.subscriberLock.Unlock()

subscriber := &Subscriber{
notifications: make(chan notifications.WorkspaceConfigNotification),
Copy link
Member

@achettyiitr achettyiitr Jan 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should a buffered channel be more appropriate here? If the consumer of the channel is blocked for some reason, it is going to block all the consumers.

@achettyiitr achettyiitr merged commit 6a9bbf4 into main Jan 4, 2024
@github-actions github-actions bot mentioned this pull request Jan 5, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants