Skip to content

Commit

Permalink
Change themes in random order once a day/hour (#62)
Browse files Browse the repository at this point in the history
  • Loading branch information
tarampampam authored Feb 1, 2022
1 parent 7e7f956 commit 375272b
Show file tree
Hide file tree
Showing 6 changed files with 254 additions and 22 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 17 additions & 1 deletion internal/cli/serve/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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")

Expand Down Expand Up @@ -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
Expand Down
11 changes: 9 additions & 2 deletions internal/cli/serve/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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(
Expand Down
41 changes: 23 additions & 18 deletions internal/pick/picker.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)))
return p.randomizeNext()

p.mu.Lock()
defer p.mu.Unlock()
default:
panic("picker.NextIndex(): unsupported mode")
}
}

if idx == p.lastIdx {
p.lastIdx++
} else {
p.lastIdx = idx
}
func (p *picker) randomizeNext() uint32 {
var idx = uint32(p.rand.Intn(int(p.maxIdx + 1)))

if p.lastIdx > p.maxIdx { // overflow?
p.lastIdx = 0
}
p.mu.Lock()
defer p.mu.Unlock()

return p.lastIdx
if idx == p.lastIdx {
p.lastIdx++
} else {
p.lastIdx = idx
}

default:
panic("picker.NextIndex(): unsupported mode")
if p.lastIdx > p.maxIdx { // overflow?
p.lastIdx = 0
}

if p.lastIdx == unsetIdx {
p.lastIdx--
}

return p.lastIdx
}
117 changes: 116 additions & 1 deletion internal/pick/strings_slice.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,25 @@
package pick

import (
"errors"
"sync"
"time"
)

type StringsSlice struct {
s []string
p *picker
}

// 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.
Expand All @@ -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
}
81 changes: 81 additions & 0 deletions internal/pick/strings_slice_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package pick_test

import (
"testing"
"time"

"github.com/stretchr/testify/assert"
"github.com/tarampampam/error-pages/internal/pick"
Expand Down Expand Up @@ -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)
})
}

0 comments on commit 375272b

Please sign in to comment.