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

Allow configurable cache watermarks #973

Merged
merged 7 commits into from
Apr 4, 2024
Merged
Show file tree
Hide file tree
Changes from 4 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
58 changes: 58 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import (
"net/url"
"os"
"path/filepath"
"slices"
"sort"
"strconv"
"strings"
Expand Down Expand Up @@ -143,6 +144,8 @@ var (
version string = "dev"

MetadataTimeoutErr *MetadataErr = &MetadataErr{msg: "Timeout when querying metadata"}

watermarkUnits = []byte{'k', 'm', 'g', 't'}
)

// This function creates a new MetadataError by wrapping the previous error
Expand Down Expand Up @@ -659,6 +662,45 @@ func handleDeprecatedConfig() {
}
}

func checkWatermark(wmStr string) (bool, int64, error) {
wmNum, err := strconv.Atoi(wmStr)
if err == nil {
if wmNum > 100 || wmNum < 0 {
return false, 0, errors.Errorf("watermark value %s must be a integer number in range [0, 100]. Refer to parameter page for details: https://docs.pelicanplatform.org/parameters#Cache-HighWatermark", wmStr)
}
return true, int64(wmNum), nil
// Not an integer number, check if it's in form of <int>k|m|g|t
} else {
if len(wmStr) < 1 {
return false, 0, errors.Errorf("watermark value %s is empty.", wmStr)
}
unit := wmStr[len(wmStr)-1]
if slices.Contains(watermarkUnits, unit) {
byteNum, err := strconv.Atoi(wmStr[:len(wmStr)-1])
// Bytes portion is not an integer
if err != nil {
return false, 0, errors.Errorf("watermark value %s is neither a percentage integer (e.g. 95) or a valid bytes. Refer to parameter page for details: https://docs.pelicanplatform.org/parameters#Cache-HighWatermark", wmStr)
} else {
switch unit {
case 'k':
return true, int64(byteNum) * 1024, nil
case 'm':
return true, int64(byteNum) * 1024 * 1024, nil
case 'g':
return true, int64(byteNum) * 1024 * 1024 * 1024, nil
case 't':
return true, int64(byteNum) * 1024 * 1024 * 1024 * 1024, nil
default:
return false, 0, errors.Errorf("watermark value %s is neither a percentage integer (e.g. 95) or a valid byte. Bytes representation is missing unit (k|m|g|t). Refer to parameter page for details: https://docs.pelicanplatform.org/parameters#Cache-HighWatermark", wmStr)
}
}
} else {
// Doesn't contain k|m|g|t suffix
return false, 0, errors.Errorf("watermark value %s is neither a percentage integer (e.g. 95) or a valid byte. Bytes representation is missing unit (k|m|g|t). Refer to parameter page for details: https://docs.pelicanplatform.org/parameters#Cache-HighWatermark", wmStr)
}
}
}

func InitConfig() {
viper.SetConfigType("yaml")
// 1) Set up defaults.yaml
Expand Down Expand Up @@ -986,6 +1028,22 @@ func InitServer(ctx context.Context, currentServers ServerType) error {
viper.SetDefault("Cache.Url", fmt.Sprintf("https://%v", param.Server_Hostname.GetString()))
}

if param.Cache_LowWatermark.IsSet() || param.Cache_HighWaterMark.IsSet() {
lowWmStr := param.Cache_LowWatermark.GetString()
highWmStr := param.Cache_HighWaterMark.GetString()
ok, highWmNum, err := checkWatermark(highWmStr)
if !ok && err != nil {
return errors.Wrap(err, "invalid Cache.HighWaterMark value")
}
ok, lowWmNum, err := checkWatermark(lowWmStr)
if !ok && err != nil {
return errors.Wrap(err, "invalid Cache.LowWatermark value")
}
if lowWmNum >= highWmNum {
return fmt.Errorf("invalid Cache.HighWaterMark and Cache.LowWatermark values. Cache.HighWaterMark must be greater than Cache.LowWaterMark. Got %s, %s", highWmStr, lowWmStr)
}
}

webPort := param.Server_WebPort.GetInt()
if webPort < 0 {
return errors.Errorf("the Server.WebPort setting of %d is invalid; TCP ports must be greater than 0", webPort)
Expand Down
111 changes: 111 additions & 0 deletions config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -342,3 +342,114 @@ func TestDiscoverFederation(t *testing.T) {
viper.Reset()
})
}

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

t.Run("empty-value", func(t *testing.T) {
ok, num, err := checkWatermark("")
assert.False(t, ok)
assert.Equal(t, int64(0), num)
assert.Error(t, err)
})

t.Run("string-value", func(t *testing.T) {
ok, num, err := checkWatermark("random")
assert.False(t, ok)
assert.Equal(t, int64(0), num)
assert.Error(t, err)
})

t.Run("integer-greater-than-100", func(t *testing.T) {
ok, num, err := checkWatermark("101")
assert.False(t, ok)
assert.Equal(t, int64(0), num)
assert.Error(t, err)
})

t.Run("integer-less-than-0", func(t *testing.T) {
ok, num, err := checkWatermark("-1")
assert.False(t, ok)
assert.Equal(t, int64(0), num)
assert.Error(t, err)
})

t.Run("decimal-fraction-value", func(t *testing.T) {
ok, num, err := checkWatermark("0.55")
assert.False(t, ok)
assert.Equal(t, int64(0), num)
assert.Error(t, err)
})

t.Run("decimal-int-value", func(t *testing.T) {
ok, num, err := checkWatermark("15.55")
assert.False(t, ok)
assert.Equal(t, int64(0), num)
assert.Error(t, err)
})

t.Run("int-value", func(t *testing.T) {
ok, num, err := checkWatermark("55")
assert.True(t, ok)
assert.Equal(t, int64(55), num)
assert.NoError(t, err)
})

t.Run("byte-value-no-unit", func(t *testing.T) {
ok, num, err := checkWatermark("105")
assert.False(t, ok)
assert.Equal(t, int64(0), num)
assert.Error(t, err)
})

t.Run("byte-value-no-value", func(t *testing.T) {
ok, num, err := checkWatermark("k")
assert.False(t, ok)
assert.Equal(t, int64(0), num)
assert.Error(t, err)
})

t.Run("byte-value-wrong-unit", func(t *testing.T) {
ok, num, err := checkWatermark("100K") // Only lower case is accepted
assert.False(t, ok)
assert.Equal(t, int64(0), num)
assert.Error(t, err)

ok, num, err = checkWatermark("100p")
assert.False(t, ok)
assert.Equal(t, int64(0), num)
assert.Error(t, err)

ok, num, err = checkWatermark("100byte")
assert.False(t, ok)
assert.Equal(t, int64(0), num)
assert.Error(t, err)

ok, num, err = checkWatermark("100bits")
assert.False(t, ok)
assert.Equal(t, int64(0), num)
assert.Error(t, err)
})

t.Run("byte-value-correct-unit", func(t *testing.T) {
ok, num, err := checkWatermark("1000k")
assert.True(t, ok)
assert.Equal(t, int64(1000*1024), num)
assert.NoError(t, err)

ok, num, err = checkWatermark("1000m")
assert.True(t, ok)
assert.Equal(t, int64(1000*1024*1024), num)
assert.NoError(t, err)

ok, num, err = checkWatermark("1000g")
assert.True(t, ok)
assert.Equal(t, int64(1000*1024*1024*1024), num)
assert.NoError(t, err)

ok, num, err = checkWatermark("1000t")
assert.True(t, ok)
assert.Equal(t, int64(1000*1024*1024*1024*1024), num)
assert.NoError(t, err)
})
}
2 changes: 2 additions & 0 deletions config/resources/defaults.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ Cache:
Port: 8442
SelfTest: true
SelfTestInterval: 15s
LowWatermark: 90
HighWaterMark: 95
LocalCache:
HighWaterMarkPercentage: 95
LowWaterMarkPercentage: 85
Expand Down
22 changes: 22 additions & 0 deletions docs/parameters.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -857,6 +857,28 @@ type: int
default: 8442
components: ["cache"]
---
name: Cache.LowWatermark
description: >-
A value of cache disk usage that stops the purging of cached files.

The value should be either a percentage integer of total avaibale disk space (default is 90),
turetske marked this conversation as resolved.
Show resolved Hide resolved
or a number suffixed by k, m, g, or t. In which case, they must be absolute sizes in k (kilo-),
m (mega-), g (giga-), or t (tera-) bytes, respectively.
type: string
default: 90
components: ["cache"]
---
name: Cache.HighWaterMark
description: >-
A value of cache disk usage that triggers the purging of cached files.

The value should be either a percentage integer of total avaibale disk space (default is 95),
turetske marked this conversation as resolved.
Show resolved Hide resolved
or a number suffixed by k, m, g, or t. In which case, they must be absolute sizes in k (kilo-),
m (mega-), g (giga-), or t (tera-) bytes, respectively.
type: string
default: 95
components: ["cache"]
---
name: Cache.EnableVoms
description: >-
Enable X.509 / VOMS-based authentication for the cache. This allows HTTP clients
Expand Down
2 changes: 2 additions & 0 deletions param/parameters.go

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

4 changes: 4 additions & 0 deletions param/parameters_struct.go

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

3 changes: 2 additions & 1 deletion xrootd/resources/xrootd-cache.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@ pfc.blocksize 128k
pfc.prefetch 20
pfc.writequeue 16 4
pfc.ram 4g
pfc.diskusage 0.90 0.95 purgeinterval 300s
pfc.diskusage {{if .Cache.LowWatermark}}{{.Cache.LowWatermark}}{{else}}0.90{{end}} {{if .Cache.HighWaterMark}}{{.Cache.HighWaterMark}}{{else}}0.95{{end}} purgeinterval 300s

{{if .Cache.Concurrency}}
xrootd.fslib throttle default
throttle.throttle concurrency {{.Cache.Concurrency}}
Expand Down
17 changes: 17 additions & 0 deletions xrootd/xrootd_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import (
"path/filepath"
"reflect"
"runtime"
"strconv"
"strings"
"text/template"
"time"
Expand Down Expand Up @@ -104,6 +105,8 @@ type (
UseCmsd bool
EnableVoms bool
CalculatedPort string
HighWaterMark string
LowWatermark string
ExportLocation string
RunLocation string
DataLocation string
Expand Down Expand Up @@ -590,6 +593,20 @@ func ConfigXrootd(ctx context.Context, origin bool) (string, error) {
return "", errors.Wrap(err, "failed to unmarshal xrootd config")
}

// For cache. convert integer percentage value [0,100] to decimal fraction [0.00, 1.00]
if !origin {
if num, err := strconv.Atoi(xrdConfig.Cache.HighWaterMark); err == nil {
if num <= 100 && num > 0 {
xrdConfig.Cache.HighWaterMark = strconv.FormatFloat(float64(num)/100, 'f', 2, 64)
}
}
if num, err := strconv.Atoi(xrdConfig.Cache.LowWatermark); err == nil {
if num <= 100 && num > 0 {
xrdConfig.Cache.LowWatermark = strconv.FormatFloat(float64(num)/100, 'f', 2, 64)
}
}
}

// To make sure we get the correct exports, we overwrite the exports in the xrdConfig struct with the exports
// we get from the common.GetOriginExports() function. Failure to do so will cause us to hit viper again,
// which in the case of tests prevents us from overwriting some exports with temp dirs.
Expand Down
Loading