Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ RUN CGO_ENABLED=0 GOOS=linux go build -o /simpleboards cmd/simpleboards/main.go

# Run the tests in the container
FROM build-stage AS run-test-stage
RUN go test -v ./...
RUN go test -v ./internal/...

# Deploy the application binary into a lean image
FROM alpine:3.18 AS build-release-stage
FROM alpine:3.19 AS build-release-stage

WORKDIR /

Expand Down
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,18 @@
- VPC
- Subnets

## Test locally with Kubernetes
### install k9s
`brew install derailed/k9s/k9s`
### Setup minikube

Ref: https://minikube.sigs.k8s.io/docs/start

- `curl -LO https://storage.googleapis.com/minikube/releases/latest/minikube-darwin-arm64`
- `sudo install minikube-darwin-arm64 /usr/local/bin/minikube`
- start the cluster: `minikube start -p simpleboards`
- install helm `minikube addons enable helm-tiller -p default`
- `minikube addons enable yakd -p default `
- `minikube -p default service yakd-dashboard -n yakd-dashboard`
- ` minikube -p default addons enable metrics-server`
- `minikube -p default dashboard --url`
45 changes: 0 additions & 45 deletions internal/adapters/output/repository/dynamodb.go
Original file line number Diff line number Diff line change
Expand Up @@ -230,50 +230,6 @@ func (r *DynamoDBRepository) MinWithMetadata(entry string, leaderboard string, v
return domain.ScoreUpdate{Score: s.Score, Done: true, Counter: s.Counter}, nil
}

func (r *DynamoDBRepository) debugExpression(expr expression.Expression, meta domain.Metadata, entry string, leaderboard string) {
ctx, cancel := context.WithTimeoutCause(context.Background(), 1*time.Second, errors.New("get configuration timeout"))
defer cancel()

keyCond := expression.KeyAnd(
expression.Key(hashKeyName).Equal(expression.Value(pkValue(entry))),
expression.Key(sortKeyName).Equal(expression.Value(skValue(leaderboard))),
)
exprq, err := expression.NewBuilder().WithKeyCondition(keyCond).Build()
if err != nil {
panic(err)
}
input := dynamodb.QueryInput{
TableName: aws.String(r.tableName),
ExpressionAttributeNames: exprq.Names(),
ExpressionAttributeValues: exprq.Values(),
KeyConditionExpression: exprq.KeyCondition(),
}

out, err := r.client.Query(ctx, &input)
if err != nil {
panic(err)
}
var it []map[string]interface{}
err = attributevalue.UnmarshalListOfMaps(out.Items, &it)

if err != nil {
panic(err)
}

fmt.Println("items:", it)

var v map[string]interface{}
attributevalue.UnmarshalMap(expr.Values(), &v)
fmt.Println()
fmt.Println()
if expr.Condition() != nil {

fmt.Println("condition:", *expr.Condition(), "names", expr.Names(), "values", v)
}
fmt.Println()
fmt.Println()
}

// LastWithMetadata ...
func (r *DynamoDBRepository) LastWithMetadata(entry string, leaderboard string, value float64, meta domain.Metadata) (domain.ScoreUpdate, error) {
builder := expression.NewBuilder()
Expand Down Expand Up @@ -457,7 +413,6 @@ func (*DynamoDBRepository) builderFromMetadata(meta domain.Metadata) expression.
expression.And(
expression.AttributeExists(expression.Name(a)),
expression.Equal(expression.Name(a), expression.Value(v))))

}

}
Expand Down
109 changes: 96 additions & 13 deletions internal/core/domain/cron.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,23 @@ package domain

import (
"fmt"
"math"
"time"

"github.com/gorhill/cronexpr"
)

type epochCache struct {
epoch int64
timestamp time.Time
}

// CronExpression data
type CronExpression struct {
expr *cronexpr.Expression
first time.Time
second time.Time
interval int64
expr *cronexpr.Expression
first time.Time
second time.Time
interval int64
lastEpochCached epochCache
}

// NewCronExpression creates a cron expression from a reset expression
Expand All @@ -32,27 +37,105 @@ func NewCronExpression(reset ResetExpression) (CronExpression, error) {
e = reset.CronExpression
}

initUnix := time.Unix(0, 0).UTC()
expr, err := cronexpr.Parse(e)
if err != nil {
return CronExpression{}, fmt.Errorf("failed to parse cron expression '%v': %v", e, err)
}

initUnix := time.Unix(0, 0).UTC()
first := expr.Next(initUnix)
second := expr.Next(first)
intervalSecs := second.Sub(first).Seconds()

return CronExpression{
expr: expr,
first: first,
second: second,
interval: int64(intervalSecs),
lec, err := calculateCurrentEpochSince(expr, initUnix)
if err != nil {
return CronExpression{}, fmt.Errorf("failed to calculate epoch: %w", err)
}
ce := CronExpression{
expr: expr,
first: first,
second: second,
interval: int64(intervalSecs),
lastEpochCached: lec,
}
return ce, nil
}

func calculateCurrentEpochSince(expr *cronexpr.Expression, since time.Time) (epochCache, error) {
now := time.Now().UTC()

ec, err := calculateEpochBetween(expr, since, now)
if err != nil {
return epochCache{}, err
}
return ec, nil
}

func calculateEpochBetween(expr *cronexpr.Expression, from time.Time, to time.Time) (epochCache, error) {
if !from.Before(to) {
return epochCache{}, fmt.Errorf("'from' time needs to be before 'to' time")
}

next := from
previous := next
var epoch int64 = 1
for next.Unix() < to.Unix() {
previous = next
next = expr.Next(next)
epoch++
}

return epochCache{
epoch: epoch,
timestamp: previous,
}, nil
}

// GetCurrentEpoch returns the epoch based in the current Timestamp
func (e *CronExpression) GetCurrentEpoch() int64 {
now := time.Now().UTC().Unix()

return e.GetEpochFromReferenceUnixTimestamp(now)
}

func (e *CronExpression) GetEpochBetweenUnixTimestamps(from int64, to int64) int64 {
epoch := e.lastEpochCached.epoch
since := e.lastEpochCached.timestamp

if from >= e.lastEpochCached.timestamp.Unix() {
// TODO: I need to review the error condition handling, not serious because is a private function
ec, _ := calculateEpochBetween(e.expr, since, time.Unix(to, 0))
epoch = epoch + ec.epoch
since = ec.timestamp
e.lastEpochCached = epochCache{epoch: epoch, timestamp: since}
} else {
initUnix := time.Unix(0, 0).UTC()
// TODO: I need to review the error condition handling, not serious because is a private function
ec, _ := calculateEpochBetween(e.expr, initUnix, time.Unix(to, 0))
epoch = ec.epoch
}

return epoch
}

// GetEpochFromReferenceUnixTimestamp calculates the epoch based on a cron expression and a ref unix timestamp
func (e *CronExpression) GetEpochFromReferenceUnixTimestamp(ref int64) int64 {
return int64(math.Floor(float64((ref-e.first.Unix())/int64(e.interval)))) + 1
epoch := e.lastEpochCached.epoch
since := e.lastEpochCached.timestamp

if ref >= e.lastEpochCached.timestamp.Unix() {
// TODO: I need to review the error condition handling, not serious because is a private function
ec, _ := calculateCurrentEpochSince(e.expr, since)
epoch = epoch + ec.epoch
since = ec.timestamp
e.lastEpochCached = epochCache{epoch: epoch, timestamp: since}
} else {
initUnix := time.Unix(0, 0).UTC()
// TODO: I need to review the error condition handling, not serious because is a private function
ec, _ := calculateCurrentEpochSince(e.expr, initUnix)
epoch = ec.epoch
}

return epoch
}

// GetNexFromNowUTC returns the next time after the current UTC timestamp
Expand Down
67 changes: 36 additions & 31 deletions internal/core/domain/cron_test.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package domain

import (
"fmt"
"math"
"testing"
"time"

Expand All @@ -12,7 +10,10 @@ import (
)

// fix the reference timestamp
const refGlobal int64 = 1719848640
const (
refGlobal int64 = 1719848640
toGlobal int64 = 1735948809
)

func TestParseCustom(t *testing.T) {
e := "00 6 * * 1"
Expand All @@ -21,8 +22,10 @@ func TestParseCustom(t *testing.T) {
Type: Custom,
CronExpression: e,
})

assert.NoError(t, err)
assert.NoError(t, err)
assert.Equal(t, int64(2844), ce.GetEpochFromReferenceUnixTimestamp(ref))
assert.Equal(t, int64(2872), ce.GetEpochBetweenUnixTimestamps(ref, toGlobal))
}

func TestParseHourly(t *testing.T) {
Expand All @@ -32,7 +35,7 @@ func TestParseHourly(t *testing.T) {
})
assert.NoError(t, err)

assert.Equal(t, int64(477735), ce.GetEpochFromReferenceUnixTimestamp(ref))
assert.Equal(t, int64(482210), ce.GetEpochBetweenUnixTimestamps(ref, toGlobal))
}

func TestParseDaily(t *testing.T) {
Expand All @@ -42,16 +45,17 @@ func TestParseDaily(t *testing.T) {
})
assert.NoError(t, err)

assert.Equal(t, int64(19905), ce.GetEpochFromReferenceUnixTimestamp(ref))
assert.Equal(t, int64(20094), ce.GetEpochBetweenUnixTimestamps(ref, toGlobal))
}

func TestParseWeekly(t *testing.T) {
ref := refGlobal
ce, err := NewCronExpression(ResetExpression{
Type: Weekly,
})
assert.NoError(t, err)

assert.Equal(t, int64(2844), ce.GetEpochFromReferenceUnixTimestamp(ref))
assert.Equal(t, int64(2872), ce.GetEpochBetweenUnixTimestamps(ref, toGlobal))
}

func TestGetNexFromRefUTC(t *testing.T) {
Expand All @@ -63,6 +67,7 @@ func TestGetNexFromRefUTC(t *testing.T) {
assert.Equal(t, time.Time(time.Date(2024, time.July, 1, 16, 0, 0, 0, time.UTC)),
ce.GetNexFromRefUTC(time.Unix(ref, 0)))
}

func TestGetNexTimestampFromRefUTC(t *testing.T) {
ref := refGlobal
ce, err := NewCronExpression(ResetExpression{
Expand All @@ -72,31 +77,31 @@ func TestGetNexTimestampFromRefUTC(t *testing.T) {
assert.Equal(t, int64(1719849600),
ce.GetNexTimestampFromRefUTC(time.Unix(ref, 0)))
}
func TestUnixTimestamp(t *testing.T) {
e := "00 6 * * 1" // every Monday at 6am
e = "* * * * *" // every minute
e = "0 * * * *" // hourly
e = "0 0 1 * *" // every 1st of month

now := time.Now().UTC().Unix()

initUnix := time.Unix(0, 0).UTC()
first := cronexpr.MustParse(e).Next(initUnix)
second := cronexpr.MustParse(e).Next(first)
intervalSecs := second.Sub(first).Seconds()
epoch := int64(math.Floor(float64((now-first.Unix())/int64(intervalSecs)))) + 1

fmt.Println(now)
fmt.Println("init:\t\t", initUnix)
fmt.Println("first:\t\t", first)
fmt.Println("second:\t\t", second)
fmt.Println("interval:\t", intervalSecs)
fmt.Println("epoch:\t\t", epoch)

assert.True(t, true)

func TestEpochByRef(t *testing.T) {
ref := refGlobal
ce, err := NewCronExpression(ResetExpression{
Type: Hourly,
})
assert.NoError(t, err)

epoch := ce.GetEpochBetweenUnixTimestamps(ref, toGlobal)
assert.Equal(t, int64(482210), epoch)
}

func TestEpochByRefBeforeCached(t *testing.T) {
ref := refGlobal
before := time.Unix(ref, 0).Add(-60 * time.Minute).UTC().Unix()
ce, err := NewCronExpression(ResetExpression{
Type: Hourly,
})
assert.NoError(t, err)

epoch := ce.GetEpochBetweenUnixTimestamps(before, toGlobal)
assert.Equal(t, int64(482210), epoch)
}
func BenchmarkPrimeNumbers(b *testing.B) {

func BenchmarkPrimeNumbers(b *testing.B) {
for i := 0; i < b.N; i++ {
e := "0 0 1 * *" // every 1st of month

Expand All @@ -106,7 +111,7 @@ func BenchmarkPrimeNumbers(b *testing.B) {
first := cronexpr.MustParse(e).Next(initUnix)
second := cronexpr.MustParse(e).Next(first)
intervalSecs := second.Sub(first).Seconds()
_ = int64(math.Floor(float64((now-first.Unix())/int64(intervalSecs)))) + 1
_ = int64(float64((now-first.Unix())/int64(intervalSecs))) + 1

}
}
4 changes: 2 additions & 2 deletions internal/core/domain/leaderboards.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,14 +61,14 @@ type LeaderboardScoreBoardConfig struct {

type ResetExpression struct {
Type LeaderboardResetType `json:"reset_type"`
CronExpression string `json:"cron, omitempty"`
CronExpression string `json:"cron,omitempty"`
}

// LeaderboardConfig holds information of a Leaderboard instance
type LeaderboardConfig struct {
Name string `json:"name"`
Function LeaderboardFunctionType `json:"function"`
ResetExpression ResetExpression `json:"reset`
ResetExpression ResetExpression `json:"reset"`
PrizeTable LeaderboardPrizeTable `json:"prizes_table"`
Scoreboards []LeaderboardScoreBoardConfig `json:"scoreboards"`
CronExpression CronExpression `json:"-"`
Expand Down
Loading