github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/core/cache/model.go (about) 1 // Copyright 2018 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package cache 5 6 import ( 7 "sort" 8 "strings" 9 "sync" 10 11 "github.com/juju/errors" 12 "github.com/juju/pubsub" 13 "gopkg.in/tomb.v2" 14 ) 15 16 const modelConfigChange = "model-config-change" 17 18 func newModel(metrics *ControllerGauges, hub *pubsub.SimpleHub) *Model { 19 m := &Model{ 20 metrics: metrics, 21 // TODO: consider a separate hub per model for better scalability 22 // when many models. 23 hub: hub, 24 applications: make(map[string]*Application), 25 } 26 return m 27 } 28 29 // Model is a cached model in the controller. The model is kept up to 30 // date with changes flowing into the cached controller. 31 type Model struct { 32 metrics *ControllerGauges 33 hub *pubsub.SimpleHub 34 mu sync.Mutex 35 36 details ModelChange 37 configHash string 38 hashCache *modelConfigHashCache 39 applications map[string]*Application 40 } 41 42 // Report returns information that is used in the dependency engine report. 43 func (m *Model) Report() map[string]interface{} { 44 m.mu.Lock() 45 defer m.mu.Unlock() 46 47 return map[string]interface{}{ 48 "name": m.details.Owner + "/" + m.details.Name, 49 "life": m.details.Life, 50 "application-count": len(m.applications), 51 } 52 } 53 54 // Application returns the application for the input name. 55 // If the application is not found, a NotFoundError is returned. 56 func (m *Model) Application(appName string) (*Application, error) { 57 m.mu.Lock() 58 defer m.mu.Unlock() 59 60 app, found := m.applications[appName] 61 if !found { 62 return nil, errors.NotFoundf("application %q", appName) 63 } 64 return app, nil 65 66 } 67 68 // updateApplication adds or updates the application in the model. 69 func (m *Model) updateApplication(ch ApplicationChange) { 70 m.mu.Lock() 71 72 app, found := m.applications[ch.Name] 73 if !found { 74 app = newApplication(m.metrics, m.hub) 75 m.applications[ch.Name] = app 76 } 77 app.setDetails(ch) 78 79 m.mu.Unlock() 80 } 81 82 // removeApplication removes the application from the model. 83 func (m *Model) removeApplication(ch RemoveApplication) { 84 m.mu.Lock() 85 delete(m.applications, ch.Name) 86 m.mu.Unlock() 87 } 88 89 // modelTopic prefixes the topic with the model UUID. 90 func (m *Model) modelTopic(topic string) string { 91 return m.details.ModelUUID + ":" + topic 92 } 93 94 func (m *Model) setDetails(details ModelChange) { 95 m.mu.Lock() 96 97 m.details = details 98 hashCache, configHash := newModelConfigHashCache(m.metrics, details.Config) 99 if configHash != m.configHash { 100 m.configHash = configHash 101 m.hashCache = hashCache 102 m.hub.Publish(m.modelTopic(modelConfigChange), hashCache) 103 } 104 105 m.mu.Unlock() 106 } 107 108 // Config returns the current model config. 109 func (m *Model) Config() map[string]interface{} { 110 m.mu.Lock() 111 m.metrics.ModelConfigReads.Inc() 112 m.mu.Unlock() 113 return m.details.Config 114 } 115 116 // WatchConfig creates a watcher for the model config. 117 // If keys are specified, the watcher is only signals a change when 118 // those keys change values. If no keys are specified, any change in the 119 // config will trigger the watcher. 120 func (m *Model) WatchConfig(keys ...string) *modelConfigWatcher { 121 // We use a single entry buffered channel for the changes. 122 // This allows the config changed handler to send a value when there 123 // is a change, but if that value hasn't been consumed before the 124 // next change, the second change is discarded. 125 sort.Strings(keys) 126 watcher := &modelConfigWatcher{ 127 keys: keys, 128 changes: make(chan struct{}, 1), 129 } 130 watcher.hash = m.hashCache.getHash(keys) 131 // Send initial event down the channel. We know that this will 132 // execute immediately because it is a buffered channel. 133 watcher.changes <- struct{}{} 134 135 unsub := m.hub.Subscribe(m.modelTopic(modelConfigChange), watcher.configChanged) 136 137 watcher.tomb.Go(func() error { 138 <-watcher.tomb.Dying() 139 unsub() 140 return nil 141 }) 142 143 return watcher 144 } 145 146 type modelConfigHashCache struct { 147 metrics *ControllerGauges 148 config map[string]interface{} 149 // The key to the hash map is the stringified keys of the watcher. 150 // They should be sorted and comma delimited. 151 hash map[string]string 152 mu sync.Mutex 153 } 154 155 func newModelConfigHashCache(metrics *ControllerGauges, config map[string]interface{}) (*modelConfigHashCache, string) { 156 configCache := &modelConfigHashCache{ 157 metrics: metrics, 158 config: config, 159 hash: make(map[string]string), 160 } 161 // Generate the hash for the entire config. 162 allHash := configCache.generateHash(nil) 163 configCache.hash[""] = allHash 164 return configCache, allHash 165 } 166 167 func (c *modelConfigHashCache) getHash(keys []string) string { 168 c.mu.Lock() 169 defer c.mu.Unlock() 170 171 key := strings.Join(keys, ",") 172 value, found := c.hash[key] 173 if found { 174 c.metrics.ModelHashCacheHit.Inc() 175 return value 176 } 177 value = c.generateHash(keys) 178 c.hash[key] = value 179 return value 180 } 181 182 func (c *modelConfigHashCache) generateHash(keys []string) string { 183 // We are generating a hash, so call it a miss. 184 c.metrics.ModelHashCacheMiss.Inc() 185 186 interested := c.config 187 if len(keys) > 0 { 188 interested = make(map[string]interface{}) 189 for _, key := range keys { 190 if value, found := c.config[key]; found { 191 interested[key] = value 192 } 193 } 194 } 195 h, err := hash(interested) 196 if err != nil { 197 logger.Errorf("invariant error - model config should be yaml serializable and hashable, %v", err) 198 return "" 199 } 200 return h 201 } 202 203 type modelConfigWatcher struct { 204 keys []string 205 hash string 206 tomb tomb.Tomb 207 changes chan struct{} 208 // We can't send down a closed channel, so protect the sending 209 // with a mutex and bool. Since you can't really even ask a channel 210 // if it is closed. 211 closed bool 212 mu sync.Mutex 213 } 214 215 func (w *modelConfigWatcher) configChanged(topic string, value interface{}) { 216 hashCache, ok := value.(*modelConfigHashCache) 217 if !ok { 218 logger.Errorf("programming error, value not a *modelConfigHashCache") 219 } 220 hash := hashCache.getHash(w.keys) 221 if hash == w.hash { 222 // Nothing that we care about has changed, so we're done. 223 return 224 } 225 // Let the listener know. 226 w.mu.Lock() 227 defer w.mu.Unlock() 228 if w.closed { 229 return 230 } 231 232 select { 233 case w.changes <- struct{}{}: 234 default: 235 // Already a pending change, so do nothing. 236 } 237 } 238 239 // Changes is part of the core watcher definition. 240 // The changes channel is never closed. 241 func (w *modelConfigWatcher) Changes() <-chan struct{} { 242 return w.changes 243 } 244 245 // Kill is part of the worker.Worker interface. 246 func (w *modelConfigWatcher) Kill() { 247 w.mu.Lock() 248 w.closed = true 249 close(w.changes) 250 w.mu.Unlock() 251 w.tomb.Kill(nil) 252 } 253 254 // Wait is part of the worker.Worker interface. 255 func (w *modelConfigWatcher) Wait() error { 256 return w.tomb.Wait() 257 } 258 259 // Stop is currently required by the Resources wrapper in the apiserver. 260 func (w *modelConfigWatcher) Stop() error { 261 w.Kill() 262 return w.Wait() 263 }