Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions aem/default/etc/aem.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ instance:
debug: false
disable_warn: true

# In-memory caching of instance values that are constant while it is running (system properties, AEM version, run modes)
cache:
enabled: true

# State checking
check:
# Time to wait before first state checking (to avoid false-positives)
Expand Down
2 changes: 1 addition & 1 deletion aemw
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
#!/usr/bin/env sh

aem "$@"
./bin/aem "$@"
4 changes: 4 additions & 0 deletions examples/docker/src/aem/default/etc/aem.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ instance:
debug: false
disable_warn: true

# In-memory caching of instance values that are constant while it is running (system properties, AEM version, run modes)
cache:
enabled: true

# State checking
check:
# Time to wait before first state checking (to avoid false-positives)
Expand Down
2 changes: 2 additions & 0 deletions pkg/cfg/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ func (c *Config) setDefaults() {

v.SetDefault("instance.status.timeout", time.Millisecond*500)

v.SetDefault("instance.cache.enabled", true)

v.SetDefault("instance.package.upload_optimized", true)

v.SetDefault("instance.package.install_recursive", true)
Expand Down
3 changes: 2 additions & 1 deletion pkg/check.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,8 +175,9 @@ func (c EventStableChecker) Check(_ CheckContext, instance Instance) CheckResult
}

nowTime := instance.Now()
timeLocation := nowTime.Location()
unstableEvents := lo.Filter(events.List, func(e osgi.Event, _ int) bool {
receivedTime := instance.Time(e.Received)
receivedTime := time.UnixMilli(e.Received).In(timeLocation)
if !receivedTime.Add(c.ReceivedMaxAge).After(nowTime) {
return false
}
Expand Down
5 changes: 5 additions & 0 deletions pkg/instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type Instance struct {
password string

local *LocalInstance
cache *InstanceCache
http *HTTP
status *Status
repo *Repo
Expand Down Expand Up @@ -81,6 +82,10 @@ func (i Instance) HTTP() *HTTP {
return i.http
}

func (i Instance) Cache() *InstanceCache {
return i.cache
}

func (i Instance) Status() *Status {
return i.status
}
Expand Down
88 changes: 88 additions & 0 deletions pkg/instance_cache.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package pkg

import (
"sync"
)

// Cache keys are centralized here to avoid accidental collisions between managers sharing the instance cache.
const (
cacheKeyStatusSystemProps = "status.system_props"
cacheKeyStatusSlingProps = "status.sling_props"
cacheKeyStatusSlingSettings = "status.sling_settings"
cacheKeyStatusAemVersion = "status.aem_version"
)

// InstanceCache holds in-memory values that are constant for a running instance
// (like system properties or AEM version) to avoid repeating HTTP requests.
// Values are never persisted; the cache lives as long as the Instance object.
type InstanceCache struct {
instance *Instance

Enabled bool

mutex sync.Mutex
entries map[string]*instanceCacheEntry
}

type instanceCacheEntry struct {
mutex sync.Mutex
loaded bool
value any
}

func NewInstanceCache(i *Instance) *InstanceCache {
cv := i.manager.aem.config.Values()

return &InstanceCache{
instance: i,

Enabled: cv.GetBool("instance.cache.enabled"),

entries: map[string]*instanceCacheEntry{},
}
}

// Clear forgets all cached values. Needed when instance state changes (start/stop).
func (c *InstanceCache) Clear() {
c.mutex.Lock()
defer c.mutex.Unlock()
c.entries = map[string]*instanceCacheEntry{}
}

func (c *InstanceCache) entry(key string) *instanceCacheEntry {
c.mutex.Lock()
defer c.mutex.Unlock()
if c.entries == nil {
c.entries = map[string]*instanceCacheEntry{}
}
result, ok := c.entries[key]
if !ok {
result = &instanceCacheEntry{}
c.entries[key] = result
}
return result
}

// InstanceCacheGet returns a cached value or computes it using the loader.
// Errors are never cached, so failures occurring while an instance is still
// starting up do not stick for the rest of the process lifetime.
// It is a package-level function as Go methods cannot have type parameters.
func InstanceCacheGet[T any](c *InstanceCache, key string, loader func() (T, error)) (T, error) {
if c == nil || !c.Enabled {
return loader()
}
e := c.entry(key)
e.mutex.Lock()
defer e.mutex.Unlock()
if e.loaded {
return e.value.(T), nil
}
value, err := loader()
if err != nil {
var zero T
return zero, err
}
e.value = value
e.loaded = true
return value, nil
}
112 changes: 112 additions & 0 deletions pkg/instance_cache_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package pkg

import (
"fmt"
"sync"
"testing"

"github.com/stretchr/testify/assert"
)

func TestInstanceCacheGetCachesValue(t *testing.T) {
t.Parallel()

cache := &InstanceCache{Enabled: true}
loads := 0
loader := func() (string, error) {
loads++
return "value", nil
}

for i := 0; i < 3; i++ {
value, err := InstanceCacheGet(cache, "key", loader)
assert.NoError(t, err)
assert.Equal(t, "value", value)
}
assert.Equal(t, 1, loads)
}

func TestInstanceCacheGetDoesNotCacheError(t *testing.T) {
t.Parallel()

cache := &InstanceCache{Enabled: true}
loads := 0
loader := func() (string, error) {
loads++
if loads == 1 {
return "", fmt.Errorf("instance not running yet")
}
return "value", nil
}

_, err := InstanceCacheGet(cache, "key", loader)
assert.Error(t, err)

value, err := InstanceCacheGet(cache, "key", loader)
assert.NoError(t, err)
assert.Equal(t, "value", value)
assert.Equal(t, 2, loads)
}

func TestInstanceCacheGetDisabled(t *testing.T) {
t.Parallel()

cache := &InstanceCache{Enabled: false}
loads := 0
loader := func() (string, error) {
loads++
return "value", nil
}

for i := 0; i < 3; i++ {
value, err := InstanceCacheGet(cache, "key", loader)
assert.NoError(t, err)
assert.Equal(t, "value", value)
}
assert.Equal(t, 3, loads)
}

func TestInstanceCacheClear(t *testing.T) {
t.Parallel()

cache := &InstanceCache{Enabled: true}
loads := 0
loader := func() (string, error) {
loads++
return "value", nil
}

_, _ = InstanceCacheGet(cache, "key", loader)
cache.Clear()
_, _ = InstanceCacheGet(cache, "key", loader)

assert.Equal(t, 2, loads)
}

func TestInstanceCacheGetConcurrently(t *testing.T) {
t.Parallel()

cache := &InstanceCache{Enabled: true}
var mutex sync.Mutex
loads := 0
loader := func() (string, error) {
mutex.Lock()
defer mutex.Unlock()
loads++
return "value", nil
}

var group sync.WaitGroup
for i := 0; i < 50; i++ {
group.Add(1)
go func() {
defer group.Done()
value, err := InstanceCacheGet(cache, "key", loader)
assert.NoError(t, err)
assert.Equal(t, "value", value)
}()
}
group.Wait()

assert.Equal(t, 1, loads)
}
1 change: 1 addition & 0 deletions pkg/instance_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,7 @@ func (im *InstanceManager) New(id, url, user, password string) *Instance {
user: user,
password: password,
}
res.cache = NewInstanceCache(res)
res.http = NewHTTP(res, url)
res.status = NewStatus(res)
res.repo = NewRepo(res)
Expand Down
2 changes: 2 additions & 0 deletions pkg/local_instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -514,6 +514,7 @@ func (li LocalInstance) Start() error {
return err
}
}
li.instance.cache.Clear()
log.Infof("%s > started", li.instance.IDColor())
return nil
}
Expand Down Expand Up @@ -687,6 +688,7 @@ func (li LocalInstance) Stop() error {
if err := cmd.Run(); err != nil {
return fmt.Errorf("%s > cannot execute stop script : %w", li.instance.IDColor(), err)
}
li.instance.cache.Clear()
log.Infof("%s > stopped", li.instance.IDColor())
return nil
}
Expand Down
4 changes: 4 additions & 0 deletions pkg/project/app_classic/aem/default/etc/aem.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ instance:
debug: false
disable_warn: true

# In-memory caching of instance values that are constant while it is running (system properties, AEM version, run modes)
cache:
enabled: true

# State checking
check:
# Time to wait before first state checking (to avoid false-positives)
Expand Down
4 changes: 4 additions & 0 deletions pkg/project/app_cloud/aem/default/etc/aem.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ instance:
debug: false
disable_warn: true

# In-memory caching of instance values that are constant while it is running (system properties, AEM version, run modes)
cache:
enabled: true

# State checking
check:
# Time to wait before first state checking (to avoid false-positives)
Expand Down
4 changes: 4 additions & 0 deletions pkg/project/instance/aem/default/etc/aem.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ instance:
debug: false
disable_warn: true

# In-memory caching of instance values that are constant while it is running (system properties, AEM version, run modes)
cache:
enabled: true

# State checking
check:
# Time to wait before first state checking (to avoid false-positives)
Expand Down
Loading
Loading