blob: 45bcc3d84919aa653fd958d74a0a7fddef5db221 [file] [log] [blame]
// Copyright 2016 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package tumble
import (
"context"
"fmt"
"html/template"
"strconv"
"go.chromium.org/luci/common/clock/clockflag"
"go.chromium.org/luci/common/logging"
"go.chromium.org/luci/server/portal"
"go.chromium.org/luci/server/settings"
)
const (
settingDisabled = "disabled"
settingEnabled = "enabled"
)
// settingsPage is a UI page to configure a static Tumble configuration.
type settingsPage struct {
portal.BasePage
}
func (settingsPage) Title(c context.Context) (string, error) {
return "Tumble settings", nil
}
func (settingsPage) Overview(c context.Context) (template.HTML, error) {
return template.HTML(`<p>Configuration parameters for the
<a href="https://github.com/luci/luci-go/tree/master/tumble">tumble
service</a> can be found in its
<a href="https://godoc.org/go.chromium.org/luci/tumble">
documentation</a>.</p>.`), nil
}
func (settingsPage) Fields(c context.Context) ([]portal.Field, error) {
return []portal.Field{
{
ID: "NumShards",
Title: "Number of shards to use",
Type: portal.FieldText,
Placeholder: strconv.FormatUint(defaultConfig.NumShards, 10),
Validator: intValidator(true),
},
{
ID: "NumGoroutines",
Title: "Number of goroutines per shard",
Type: portal.FieldText,
Placeholder: strconv.Itoa(defaultConfig.NumGoroutines),
Validator: intValidator(true),
},
{
ID: "TemporalMinDelay",
Title: "Temporal minimum delay (s, m, h)",
Type: portal.FieldText,
Placeholder: defaultConfig.TemporalMinDelay.String(),
Validator: validateDuration,
},
{
ID: "TemporalRoundFactor",
Title: "Temporal round factor, for batching (s, m, h)",
Type: portal.FieldText,
Placeholder: defaultConfig.TemporalRoundFactor.String(),
Validator: validateDuration,
},
{
ID: "ProcessLoopDuration",
Title: "Maximum lifetime of batch processing loop",
Type: portal.FieldText,
Placeholder: defaultConfig.ProcessLoopDuration.String(),
Validator: validateDuration,
},
{
ID: "DustSettleTimeout",
Title: "Amount of time to wait for datastore to settle in between mutation rounds (s, m, h)",
Type: portal.FieldText,
Placeholder: defaultConfig.DustSettleTimeout.String(),
Validator: validateDuration,
},
{
ID: "MaxNoWorkDelay",
Title: "Maximum amount of time to sleep in between rounds if here was no work done " +
"the previous round (s, m, h)",
Type: portal.FieldText,
Placeholder: defaultConfig.MaxNoWorkDelay.String(),
Validator: validateDuration,
},
{
ID: "NoWorkDelayGrowth",
Title: "Growth factor for the delay in between loops when no work was done. " +
"If <= 1, no growth will be applied. The delay is capped by MaxNoWorkDelay.",
Type: portal.FieldText,
Placeholder: strconv.Itoa(int(defaultConfig.NoWorkDelayGrowth)),
Validator: intValidator(true),
},
{
ID: "ProcessMaxBatchSize",
Title: "Number of mutations to include per commit (negative for unlimited)",
Type: portal.FieldText,
Placeholder: strconv.Itoa(int(defaultConfig.ProcessMaxBatchSize)),
Validator: intValidator(false),
},
{
ID: "DelayedMutations",
Title: "Delayed mutations (index MUST be present)",
Type: portal.FieldChoice,
ChoiceVariants: []string{settingDisabled, settingEnabled},
},
}, nil
}
func (settingsPage) ReadSettings(c context.Context) (map[string]string, error) {
var cfg Config
switch err := settings.GetUncached(c, baseName, &cfg); err {
case nil:
break
case settings.ErrNoSettings:
logging.WithError(err).Infof(c, "No settings available, using defaults.")
cfg = defaultConfig
default:
return nil, err
}
values := map[string]string{}
// Only render values if they differ from our default config.
if cfg.NumShards != defaultConfig.NumShards {
values["NumShards"] = strconv.FormatUint(cfg.NumShards, 10)
}
if cfg.NumGoroutines != defaultConfig.NumGoroutines {
values["NumGoroutines"] = strconv.Itoa(cfg.NumGoroutines)
}
if cfg.TemporalMinDelay != defaultConfig.TemporalMinDelay {
values["TemporalMinDelay"] = cfg.TemporalMinDelay.String()
}
if cfg.TemporalRoundFactor != defaultConfig.TemporalRoundFactor {
values["TemporalRoundFactor"] = cfg.TemporalRoundFactor.String()
}
if cfg.ProcessLoopDuration != defaultConfig.ProcessLoopDuration {
values["ProcessLoopDuration"] = cfg.ProcessLoopDuration.String()
}
if cfg.DustSettleTimeout != defaultConfig.DustSettleTimeout {
values["DustSettleTimeout"] = cfg.DustSettleTimeout.String()
}
if cfg.MaxNoWorkDelay != defaultConfig.MaxNoWorkDelay {
values["MaxNoWorkDelay"] = cfg.MaxNoWorkDelay.String()
}
if cfg.NoWorkDelayGrowth != defaultConfig.NoWorkDelayGrowth {
values["NoWorkDelayGrowth"] = strconv.Itoa(int(cfg.NoWorkDelayGrowth))
}
if cfg.ProcessMaxBatchSize != defaultConfig.ProcessMaxBatchSize {
values["ProcessMaxBatchSize"] = strconv.Itoa(int(cfg.ProcessMaxBatchSize))
}
values["DelayedMutations"] = getToggleSetting(cfg.DelayedMutations)
return values, nil
}
func (settingsPage) WriteSettings(c context.Context, values map[string]string, who, why string) error {
// Start with our default config and shape it with populated values.
cfg := defaultConfig
var err error
if v := values["NumShards"]; v != "" {
cfg.NumShards, err = strconv.ParseUint(v, 10, 64)
if err != nil {
return fmt.Errorf("could not parse NumShards: %v", err)
}
}
if v := values["NumGoroutines"]; v != "" {
cfg.NumGoroutines, err = strconv.Atoi(v)
if err != nil {
return fmt.Errorf("could not parse NumGoroutines: %v", err)
}
}
if v := values["TemporalMinDelay"]; v != "" {
cfg.TemporalMinDelay, err = clockflag.ParseDuration(v)
if err != nil {
return fmt.Errorf("could not parse TemporalMinDelay: %v", err)
}
}
if v := values["TemporalRoundFactor"]; v != "" {
cfg.TemporalRoundFactor, err = clockflag.ParseDuration(v)
if err != nil {
return fmt.Errorf("could not parse TemporalRoundFactor: %v", err)
}
}
if v := values["ProcessLoopDuration"]; v != "" {
cfg.ProcessLoopDuration, err = clockflag.ParseDuration(v)
if err != nil {
return fmt.Errorf("could not parse ProcessLoopDuration: %v", err)
}
}
if v := values["DustSettleTimeout"]; v != "" {
cfg.DustSettleTimeout, err = clockflag.ParseDuration(v)
if err != nil {
return fmt.Errorf("could not parse DustSettleTimeout: %v", err)
}
}
if v := values["MaxNoWorkDelay"]; v != "" {
cfg.MaxNoWorkDelay, err = clockflag.ParseDuration(v)
if err != nil {
return fmt.Errorf("could not parse MaxNoWorkDelay: %v", err)
}
}
if v := values["NoWorkDelayGrowth"]; v != "" {
val, err := strconv.Atoi(v)
if err != nil {
return fmt.Errorf("could not parse ProcessMaxBatchSize: %v", err)
}
cfg.NoWorkDelayGrowth = val
}
if v := values["ProcessMaxBatchSize"]; v != "" {
val, err := strconv.Atoi(v)
if err != nil {
return fmt.Errorf("could not parse ProcessMaxBatchSize: %v", err)
}
cfg.ProcessMaxBatchSize = val
}
cfg.DelayedMutations = values["DelayedMutations"] == settingEnabled
return settings.SetIfChanged(c, baseName, &cfg, who, why)
}
func intValidator(positive bool) func(string) error {
return func(v string) error {
if v == "" {
return nil
}
i, err := strconv.Atoi(v)
if err != nil {
return fmt.Errorf("invalid integer %q - %s", v, err)
}
if positive && i <= 0 {
return fmt.Errorf("value %q must be positive", v)
}
return nil
}
}
func validateDuration(v string) error {
if v == "" {
return nil
}
var cf clockflag.Duration
if err := cf.Set(v); err != nil {
return fmt.Errorf("bad duration %q - %s", v, err)
}
if cf <= 0 {
return fmt.Errorf("duration %q must be positive", v)
}
return nil
}
func getToggleSetting(v bool) string {
if v {
return settingEnabled
}
return settingDisabled
}
func init() {
portal.RegisterPage("tumble", settingsPage{})
}