Skip to content

Commit

Permalink
Merge pull request juju#18757 from SimonRichardson/application-config…
Browse files Browse the repository at this point in the history
…-watcher

juju#18757

Adds the application config watchers. There are currently two watchers:

 1. Application config watcher as notify watcher
 2. Application config hash watcher as a strings watcher

To drive these watchers, we need to know when the config has changed. Unfortunately, there are two tables to watch (config and settings), which would require a bit of additional complexity, and to drive the second watcher we would need to compute the hash for every change. Instead, we can invert that model and store the hash when the config has changed. Thus, we only need to watch when the config hash has changed, and that then can drive the watcher correctly.

For the notify watcher, we watch the application_config_hash table, for the strings watcher we also watch the same table, but get the hash instead of the UUID for the table and only send one event back. This coalescing ensures that we only send the event once and that the event has the correct sha256 for the config, which is a requirement for the uniter.

----

This pull request introduces several changes to the `domain/application/service` and `domain/application/state` packages to add functionality for managing and watching application configuration hashes. The most important changes include adding methods for retrieving and watching application configuration hashes, updating the state management to handle these hashes, and adding corresponding tests.

### Application Service Enhancements:
* Added `InitialWatchStatementApplicationConfigHash` method to `AtomicApplicationState` and `ApplicationState` interfaces to return the initial namespace query for the application config hash watcher. [[1]](diffhunk://#diff-40e92dad177289bb0953c58a7bf643aa27252c1125bdda8628c3b1029996c639R133-R136) [[2]](diffhunk://#diff-40e92dad177289bb0953c58a7bf643aa27252c1125bdda8628c3b1029996c639R297-R302)
* Implemented `WatchApplicationConfig` and `WatchApplicationConfigHash` methods in `WatchableService` to watch for changes to the application's config and config hash, respectively.

### State Management Enhancements:
* Added `GetApplicationConfigHash` method to retrieve the SHA256 hash of the application config for a specified application ID.
* Implemented `InitialWatchStatementApplicationConfigHash` method to return the initial namespace query for the application config hash watcher.
* Added `insertApplicationConfigHash` method to insert the application config hash into the database.

### Testing Enhancements:
* Added mock methods for `GetApplicationConfigHash` and `InitialWatchStatementApplicationConfigHash` to the mock state in `package_mock_test.go`. [[1]](diffhunk://#diff-b3512b9d5695360184408eef139ff1a37434dff010b323eb51922a0d038f0f0fR298-R336) [[2]](diffhunk://#diff-b3512b9d5695360184408eef139ff1a37434dff010b323eb51922a0d038f0f0fR1868-R1906)
* Added tests for `GetApplicationConfigHash` and scenarios where the application is not found in `application_test.go`.

### Miscellaneous:
* Updated `CreateApplication` and `SetApplicationConfigAndSettings` methods to handle the insertion and updating of application config hashes. [[1]](diffhunk://#diff-994df01f80f2f62fa018f73b1f15b583e059dd830bda4c524f4824df8764bb08R142-R146) [[2]](diffhunk://#diff-994df01f80f2f62fa018f73b1f15b583e059dd830bda4c524f4824df8764bb08R2521-R2532)
* Added utility functions `hashConfigAndSettings` and `encodeConfigValue` to generate SHA256 hashes for application configurations.

## QA

Tests pass.

## Links


**Jira card:** [JUJU-7414](https://warthogs.atlassian.net/browse/JUJU-7414)



[JUJU-7414]: https://warthogs.atlassian.net/browse/JUJU-7414?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
  • Loading branch information
jujubot authored Feb 4, 2025
2 parents 671b2cf + ed95ff7 commit 28502a6
Show file tree
Hide file tree
Showing 11 changed files with 712 additions and 3 deletions.
10 changes: 10 additions & 0 deletions domain/application/service/application.go
Original file line number Diff line number Diff line change
Expand Up @@ -260,13 +260,23 @@ type ApplicationState interface {
// SetUnitLife sets the life of the specified unit.
SetUnitLife(context.Context, coreunit.Name, life.Life) error

// GetApplicationConfigHash returns the SHA256 hash of the application config
// for the specified application ID.
// If no application is found, an error satisfying
// [applicationerrors.ApplicationNotFound] is returned.
GetApplicationConfigHash(ctx context.Context, appID coreapplication.ID) (string, error)

// InitialWatchStatementUnitLife returns the initial namespace query for the
// application unit life watcher.
InitialWatchStatementUnitLife(appName string) (string, eventsource.NamespaceQuery)

// InitialWatchStatementApplicationsWithPendingCharms returns the initial
// namespace query for the applications with pending charms watcher.
InitialWatchStatementApplicationsWithPendingCharms() (string, eventsource.NamespaceQuery)

// InitialWatchStatementApplicationConfigHash returns the initial namespace
// query for the application config hash watcher.
InitialWatchStatementApplicationConfigHash(appName string) (string, eventsource.NamespaceQuery)
}

// CreateApplication creates the specified application and units if required,
Expand Down
78 changes: 78 additions & 0 deletions domain/application/service/package_mock_test.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

106 changes: 105 additions & 1 deletion domain/application/service/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,6 @@ func (s *WatchableService) WatchApplicationScale(ctx context.Context, appName st
// WatchApplicationsWithPendingCharms returns a watcher that observes changes to
// applications that have pending charms.
func (s *WatchableService) WatchApplicationsWithPendingCharms(ctx context.Context) (watcher.StringsWatcher, error) {

table, query := s.st.InitialWatchStatementApplicationsWithPendingCharms()
return s.watcherFactory.NewNamespaceMapperWatcher(
table,
Expand Down Expand Up @@ -336,6 +335,8 @@ type indexedChanged struct {

// WatchApplication watches for changes to the specified application in the
// application table.
// If the application does not exist an error satisfying
// [applicationerrors.NotFound] will be returned.
func (s *WatchableService) WatchApplication(ctx context.Context, name string) (watcher.NotifyWatcher, error) {
uuid, err := s.GetApplicationIDByName(ctx, name)
if err != nil {
Expand All @@ -348,6 +349,109 @@ func (s *WatchableService) WatchApplication(ctx context.Context, name string) (w
)
}

// WatchApplicationConfig watches for changes to the specified application's
// config.
// This notifies on any changes to the application's config, which is driven
// of the application config hash. It is up to the caller to determine if the
// config value they're interested in has changed.
//
// If the application does not exist an error satisfying
// [applicationerrors.NotFound] will be returned.
func (s *WatchableService) WatchApplicationConfig(ctx context.Context, name string) (watcher.NotifyWatcher, error) {
uuid, err := s.GetApplicationIDByName(ctx, name)
if err != nil {
return nil, internalerrors.Errorf("getting ID of application %s: %w", name, err)
}

return s.watcherFactory.NewValueWatcher("application_config_hash", uuid.String(), changestream.All)
}

// WatchApplicationConfigHash watches for changes to the specified application's
// config hash.
// This notifies on any changes to the application's config hash, which is
// driven of the application config hash. It is up to the caller to determine
// if the config value they're interested in has changed. This watcher is
// the backing for the uniter's remote state. We should be attempting to
// remove this in the future.
//
// If the application does not exist an error satisfying
// [applicationerrors.NotFound] will be returned.
func (s *WatchableService) WatchApplicationConfigHash(ctx context.Context, name string) (watcher.StringsWatcher, error) {
appID, err := s.GetApplicationIDByName(ctx, name)
if err != nil {
return nil, internalerrors.Errorf("getting ID of application %s: %w", name, err)
}

// sha256 is the current config hash for the application. This will
// be filled in by the initial query. If it's empty after the initial
// query, then a new config hash will be generated on the first change.
var sha256 string

table, query := s.st.InitialWatchStatementApplicationConfigHash(name)
return s.watcherFactory.NewNamespaceMapperWatcher(
table,
changestream.All,
func(ctx context.Context, txn database.TxnRunner) ([]string, error) {
initialResults, err := query(ctx, txn)
if err != nil {
return nil, errors.Trace(err)
}

if num := len(initialResults); num > 1 {
return nil, internalerrors.Errorf("too many config hashes for application %q", name)
} else if num == 1 {
sha256 = initialResults[0]
}

return initialResults, nil
},
func(ctx context.Context, _ database.TxnRunner, changes []changestream.ChangeEvent) ([]changestream.ChangeEvent, error) {
// If there are no changes, return no changes.
if len(changes) == 0 {
return nil, nil
}

currentSHA256, err := s.st.GetApplicationConfigHash(ctx, appID)
if err != nil {
return nil, errors.Trace(err)
}
// If the hash hasn't changed, return no changes. The first sha256
// might be empty, so if that's the case the currentSHA256 will not
// be empty. Either way we'll only return changes if the hash has
// changed.
if currentSHA256 == sha256 {
return nil, nil
}
sha256 = currentSHA256

// There can be only one.
// Select the last change event, which will be naturally ordered
// by the grouping of the query (CREATE, UPDATE, DELETE).
change := changes[len(changes)-1]

return []changestream.ChangeEvent{
newMaskedChangeIDEvent(change, sha256),
}, nil
},
)
}

type maskedChangeIDEvent struct {
changestream.ChangeEvent
id string
}

func newMaskedChangeIDEvent(change changestream.ChangeEvent, id string) changestream.ChangeEvent {
return maskedChangeIDEvent{
ChangeEvent: change,
id: id,
}
}

func (m maskedChangeIDEvent) Changed() string {
return m.id
}

// isValidApplicationName returns whether name is a valid application name.
func isValidApplicationName(name string) bool {
return validApplication.MatchString(name)
Expand Down
Loading

0 comments on commit 28502a6

Please sign in to comment.