From 375272b5618b6d32ad4630659a93ea879b960eaf Mon Sep 17 00:00:00 2001 From: Paramtamtam <7326800+tarampampam@users.noreply.github.com> Date: Tue, 1 Feb 2022 19:39:50 +0500 Subject: [PATCH] Change themes in random order once a day/hour (#62) --- CHANGELOG.md | 8 ++ internal/cli/serve/command.go | 18 ++++- internal/cli/serve/flags.go | 11 ++- internal/pick/picker.go | 45 ++++++----- internal/pick/strings_slice.go | 117 +++++++++++++++++++++++++++- internal/pick/strings_slice_test.go | 81 +++++++++++++++++++ 6 files changed, 256 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 478e3a1..d9cc6df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,14 @@ All notable changes to this package will be documented in this file. The format is based on [Keep a Changelog][keepachangelog] and this project adheres to [Semantic Versioning][semver]. +## UNRELEASED + +### Added + +- Possibility to change the template to the random once a day using "special" template name `random-daily` (or hourly, using `random-hourly`) [#48] + +[#48]:https://github.com/tarampampam/error-pages/issues/48 + ## v2.5.0 ### Changed diff --git a/internal/cli/serve/command.go b/internal/cli/serve/command.go index 85e32f4..e73be58 100644 --- a/internal/cli/serve/command.go +++ b/internal/cli/serve/command.go @@ -69,7 +69,7 @@ func run(parentCtx context.Context, log *zap.Logger, f flags, cfg *config.Config var ( templateNames = cfg.TemplateNames() - picker *pick.StringsSlice + picker interface{ Pick() string } ) switch f.template.name { @@ -83,6 +83,16 @@ func run(parentCtx context.Context, log *zap.Logger, f flags, cfg *config.Config picker = pick.NewStringsSlice(templateNames, pick.RandomEveryTime) + case useRandomTemplateDaily: + log.Info("A random template will be used and changed once a day") + + picker = pick.NewStringsSliceWithInterval(templateNames, pick.RandomEveryTime, time.Hour*24) //nolint:gomnd + + case useRandomTemplateHourly: + log.Info("A random template will be used and changed hourly") + + picker = pick.NewStringsSliceWithInterval(templateNames, pick.RandomEveryTime, time.Hour) + case "": log.Info("The first template (ordered by name) will be used") @@ -132,6 +142,12 @@ func run(parentCtx context.Context, log *zap.Logger, f flags, cfg *config.Config case <-ctx.Done(): // ..or context cancellation log.Info("Gracefully server stopping", zap.Duration("uptime", time.Since(startedAt))) + if p, ok := picker.(interface{ Close() error }); ok { + if err := p.Close(); err != nil { + return err + } + } + // stop the server using created context above if err := server.Stop(); err != nil { return err diff --git a/internal/cli/serve/flags.go b/internal/cli/serve/flags.go index e138f59..b474c99 100644 --- a/internal/cli/serve/flags.go +++ b/internal/cli/serve/flags.go @@ -35,6 +35,8 @@ const ( const ( useRandomTemplate = "random" useRandomTemplateOnEachRequest = "i-said-random" + useRandomTemplateDaily = "random-daily" + useRandomTemplateHourly = "random-hourly" ) func (f *flags) init(flagSet *pflag.FlagSet) { @@ -55,8 +57,13 @@ func (f *flags) init(flagSet *pflag.FlagSet) { templateNameFlagName, "t", "", fmt.Sprintf( - "template name (set \"%s\" to use a randomized or \"%s\" to use a randomized template on each request) [$%s]", //nolint:lll - useRandomTemplate, useRandomTemplateOnEachRequest, env.TemplateName, + "template name (set \"%s\" to use a randomized or \"%s\" to use a randomized template on each request "+ + "or \"%s/%s\" daily/hourly randomized) [$%s]", + useRandomTemplate, + useRandomTemplateOnEachRequest, + useRandomTemplateDaily, + useRandomTemplateHourly, + env.TemplateName, ), ) flagSet.StringVarP( diff --git a/internal/pick/picker.go b/internal/pick/picker.go index be04aa7..8f5183f 100644 --- a/internal/pick/picker.go +++ b/internal/pick/picker.go @@ -51,33 +51,38 @@ func (p *picker) NextIndex() uint32 { case RandomOnce: if p.lastIdx == unsetIdx { - p.mu.Lock() - defer p.mu.Unlock() - - p.lastIdx = uint32(p.rand.Intn(int(p.maxIdx))) + return p.randomizeNext() } return p.lastIdx case RandomEveryTime: - var idx = uint32(p.rand.Intn(int(p.maxIdx + 1))) - - p.mu.Lock() - defer p.mu.Unlock() - - if idx == p.lastIdx { - p.lastIdx++ - } else { - p.lastIdx = idx - } - - if p.lastIdx > p.maxIdx { // overflow? - p.lastIdx = 0 - } - - return p.lastIdx + return p.randomizeNext() default: panic("picker.NextIndex(): unsupported mode") } } + +func (p *picker) randomizeNext() uint32 { + var idx = uint32(p.rand.Intn(int(p.maxIdx + 1))) + + p.mu.Lock() + defer p.mu.Unlock() + + if idx == p.lastIdx { + p.lastIdx++ + } else { + p.lastIdx = idx + } + + if p.lastIdx > p.maxIdx { // overflow? + p.lastIdx = 0 + } + + if p.lastIdx == unsetIdx { + p.lastIdx-- + } + + return p.lastIdx +} diff --git a/internal/pick/strings_slice.go b/internal/pick/strings_slice.go index b6d0b93..114aa87 100644 --- a/internal/pick/strings_slice.go +++ b/internal/pick/strings_slice.go @@ -1,5 +1,11 @@ package pick +import ( + "errors" + "sync" + "time" +) + type StringsSlice struct { s []string p *picker @@ -7,7 +13,13 @@ type StringsSlice struct { // NewStringsSlice creates new StringsSlice. func NewStringsSlice(items []string, mode pickMode) *StringsSlice { - return &StringsSlice{s: items, p: NewPicker(uint32(len(items)-1), mode)} + maxIdx := len(items) - 1 + + if maxIdx < 0 { + maxIdx = 0 + } + + return &StringsSlice{s: items, p: NewPicker(uint32(maxIdx), mode)} } // Pick an element from the strings slice. @@ -18,3 +30,106 @@ func (s *StringsSlice) Pick() string { return s.s[s.p.NextIndex()] } + +type StringsSliceWithInterval struct { + s []string + p *picker + d time.Duration + + idxMu sync.RWMutex + idx uint32 + + close chan struct{} + closedMu sync.RWMutex + closed bool +} + +// NewStringsSliceWithInterval creates new StringsSliceWithInterval. +func NewStringsSliceWithInterval(items []string, mode pickMode, interval time.Duration) *StringsSliceWithInterval { + maxIdx := len(items) - 1 + + if maxIdx < 0 { + maxIdx = 0 + } + + if interval <= time.Duration(0) { + panic("NewStringsSliceWithInterval: wrong interval") + } + + s := &StringsSliceWithInterval{ + s: items, + p: NewPicker(uint32(maxIdx), mode), + d: interval, + close: make(chan struct{}, 1), + } + + s.next() + + go s.rotate() + + return s +} + +func (s *StringsSliceWithInterval) rotate() { + defer close(s.close) + + timer := time.NewTimer(s.d) + defer timer.Stop() + + for { + select { + case <-s.close: + return + + case <-timer.C: + s.next() + timer.Reset(s.d) + } + } +} + +func (s *StringsSliceWithInterval) next() { + idx := s.p.NextIndex() + + s.idxMu.Lock() + s.idx = idx + s.idxMu.Unlock() +} + +// Pick an element from the strings slice. +func (s *StringsSliceWithInterval) Pick() string { + if s.isClosed() { + panic("StringsSliceWithInterval.Pick(): closed") + } + + if len(s.s) == 0 { + return "" + } + + s.idxMu.RLock() + defer s.idxMu.RUnlock() + + return s.s[s.idx] +} + +func (s *StringsSliceWithInterval) isClosed() (closed bool) { + s.closedMu.RLock() + closed = s.closed + s.closedMu.RUnlock() + + return +} + +func (s *StringsSliceWithInterval) Close() error { + if s.isClosed() { + return errors.New("closed") + } + + s.closedMu.Lock() + s.closed = true + s.closedMu.Unlock() + + s.close <- struct{}{} + + return nil +} diff --git a/internal/pick/strings_slice_test.go b/internal/pick/strings_slice_test.go index baa2dca..6449467 100644 --- a/internal/pick/strings_slice_test.go +++ b/internal/pick/strings_slice_test.go @@ -2,6 +2,7 @@ package pick_test import ( "testing" + "time" "github.com/stretchr/testify/assert" "github.com/tarampampam/error-pages/internal/pick" @@ -47,3 +48,83 @@ func TestStringsSlice_Pick(t *testing.T) { } }) } + +func TestNewStringsSliceWithInterval_Pick(t *testing.T) { + t.Run("first", func(t *testing.T) { + for i := uint8(0); i < 50; i++ { + p := pick.NewStringsSliceWithInterval([]string{}, pick.First, time.Millisecond) + assert.Equal(t, "", p.Pick()) + assert.NoError(t, p.Close()) + assert.Panics(t, func() { p.Pick() }) + } + + p := pick.NewStringsSliceWithInterval([]string{"foo", "bar", "baz"}, pick.First, time.Millisecond) + + for i := uint8(0); i < 50; i++ { + assert.Equal(t, "foo", p.Pick()) + + <-time.After(time.Millisecond * 2) + } + + assert.NoError(t, p.Close()) + assert.Error(t, p.Close()) + assert.Panics(t, func() { p.Pick() }) + }) + + t.Run("random once", func(t *testing.T) { + for i := uint8(0); i < 50; i++ { + p := pick.NewStringsSliceWithInterval([]string{}, pick.RandomOnce, time.Millisecond) + assert.Equal(t, "", p.Pick()) + assert.NoError(t, p.Close()) + assert.Panics(t, func() { p.Pick() }) + } + + var ( + p = pick.NewStringsSliceWithInterval([]string{"foo", "bar", "baz"}, pick.RandomOnce, time.Millisecond) + picked = p.Pick() + ) + + for i := uint8(0); i < 50; i++ { + assert.Equal(t, picked, p.Pick()) + + <-time.After(time.Millisecond * 2) + } + + assert.NoError(t, p.Close()) + assert.Error(t, p.Close()) + assert.Panics(t, func() { p.Pick() }) + }) + + t.Run("random every time", func(t *testing.T) { + for i := uint8(0); i < 50; i++ { + p := pick.NewStringsSliceWithInterval([]string{}, pick.RandomEveryTime, time.Millisecond) + assert.Equal(t, "", p.Pick()) + assert.NoError(t, p.Close()) + assert.Panics(t, func() { p.Pick() }) + } + + var changed int + + for i := uint8(0); i < 50; i++ { + p := pick.NewStringsSliceWithInterval([]string{"foo", "bar", "baz"}, pick.RandomEveryTime, time.Millisecond) //nolint:lll + + one, two := p.Pick(), p.Pick() + assert.Equal(t, one, two) + + <-time.After(time.Millisecond * 2) + + three, four := p.Pick(), p.Pick() + assert.Equal(t, three, four) + + if one != three { + changed++ + } + + assert.NoError(t, p.Close()) + assert.Error(t, p.Close()) + assert.Panics(t, func() { p.Pick() }) + } + + assert.GreaterOrEqual(t, changed, 25) + }) +}