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].
|
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
|
||||||
|
@ -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
|
||||||
|
@ -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(
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user