Change themes in random order once a day/hour (#62)

This commit is contained in:
Paramtamtam 2022-02-01 19:39:50 +05:00 committed by GitHub
parent 7e7f956fae
commit 375272b561
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 256 additions and 24 deletions

View File

@ -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]. 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 ## v2.5.0
### Changed ### Changed

View File

@ -69,7 +69,7 @@ func run(parentCtx context.Context, log *zap.Logger, f flags, cfg *config.Config
var ( var (
templateNames = cfg.TemplateNames() templateNames = cfg.TemplateNames()
picker *pick.StringsSlice picker interface{ Pick() string }
) )
switch f.template.name { 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) 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 "": case "":
log.Info("The first template (ordered by name) will be used") 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 case <-ctx.Done(): // ..or context cancellation
log.Info("Gracefully server stopping", zap.Duration("uptime", time.Since(startedAt))) 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 // stop the server using created context above
if err := server.Stop(); err != nil { if err := server.Stop(); err != nil {
return err return err

View File

@ -35,6 +35,8 @@ const (
const ( const (
useRandomTemplate = "random" useRandomTemplate = "random"
useRandomTemplateOnEachRequest = "i-said-random" useRandomTemplateOnEachRequest = "i-said-random"
useRandomTemplateDaily = "random-daily"
useRandomTemplateHourly = "random-hourly"
) )
func (f *flags) init(flagSet *pflag.FlagSet) { func (f *flags) init(flagSet *pflag.FlagSet) {
@ -55,8 +57,13 @@ func (f *flags) init(flagSet *pflag.FlagSet) {
templateNameFlagName, "t", templateNameFlagName, "t",
"", "",
fmt.Sprintf( fmt.Sprintf(
"template name (set \"%s\" to use a randomized or \"%s\" to use a randomized template on each request) [$%s]", //nolint:lll "template name (set \"%s\" to use a randomized or \"%s\" to use a randomized template on each request "+
useRandomTemplate, useRandomTemplateOnEachRequest, env.TemplateName, "or \"%s/%s\" daily/hourly randomized) [$%s]",
useRandomTemplate,
useRandomTemplateOnEachRequest,
useRandomTemplateDaily,
useRandomTemplateHourly,
env.TemplateName,
), ),
) )
flagSet.StringVarP( flagSet.StringVarP(

View File

@ -51,33 +51,38 @@ func (p *picker) NextIndex() uint32 {
case RandomOnce: case RandomOnce:
if p.lastIdx == unsetIdx { if p.lastIdx == unsetIdx {
p.mu.Lock() return p.randomizeNext()
defer p.mu.Unlock()
p.lastIdx = uint32(p.rand.Intn(int(p.maxIdx)))
} }
return p.lastIdx return p.lastIdx
case RandomEveryTime: case RandomEveryTime:
var idx = uint32(p.rand.Intn(int(p.maxIdx + 1))) return p.randomizeNext()
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
default: default:
panic("picker.NextIndex(): unsupported mode") 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
}

View File

@ -1,5 +1,11 @@
package pick package pick
import (
"errors"
"sync"
"time"
)
type StringsSlice struct { type StringsSlice struct {
s []string s []string
p *picker p *picker
@ -7,7 +13,13 @@ type StringsSlice struct {
// NewStringsSlice creates new StringsSlice. // NewStringsSlice creates new StringsSlice.
func NewStringsSlice(items []string, mode pickMode) *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. // Pick an element from the strings slice.
@ -18,3 +30,106 @@ func (s *StringsSlice) Pick() string {
return s.s[s.p.NextIndex()] 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
}

View File

@ -2,6 +2,7 @@ package pick_test
import ( import (
"testing" "testing"
"time"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/tarampampam/error-pages/internal/pick" "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)
})
}