mirror of
https://github.com/tarampampam/error-pages.git
synced 2024-08-30 18:22:40 +00:00
Change themes in random order once a day/hour (#62)
This commit is contained in:
parent
7e7f956fae
commit
375272b561
@ -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
|
||||
|
@ -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
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user