agola/internal/sequence/sequence.go

133 lines
3.4 KiB
Go

// Copyright 2019 Sorint.lab
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied
// See the License for the specific language governing permissions and
// limitations under the License.
package sequence
import (
"context"
"encoding/json"
"fmt"
"math"
"strconv"
"strings"
"time"
"agola.io/agola/internal/errors"
"agola.io/agola/internal/etcd"
)
type Sequence struct {
Epoch uint64
C uint64
}
func (s *Sequence) String() string {
// 1<<64 -1 in base 32 is "fvvvvvvvvvvvv" and uses 13 chars
return fmt.Sprintf("%013s-%013s", strconv.FormatUint(s.Epoch, 32), strconv.FormatUint(s.C, 32))
}
func (s *Sequence) Reverse() *Sequence {
return &Sequence{
Epoch: math.MaxUint64 - s.Epoch,
C: math.MaxUint64 - s.C,
}
}
func Parse(s string) (*Sequence, error) {
if len(s) != 13*2+1 {
return nil, errors.Errorf("bad sequence %q string length", s)
}
parts := strings.Split(s, "-")
if len(parts) != 2 {
return nil, errors.Errorf("bad sequence %q", s)
}
epoch, err := strconv.ParseUint(parts[0], 32, 64)
if err != nil {
return nil, errors.Wrapf(err, "cannot parse sequence epoch %q", epoch)
}
c, err := strconv.ParseUint(parts[1], 32, 64)
if err != nil {
return nil, errors.Wrapf(err, "cannot parse sequence count %q", c)
}
return &Sequence{
Epoch: epoch,
C: c,
}, nil
}
func (s *Sequence) EqualEpoch(s2 *Sequence) bool {
return s.Epoch == s2.Epoch
}
func CurSequence(ctx context.Context, e *etcd.Store, key string) (*Sequence, bool, error) {
resp, err := e.Get(ctx, key, 0)
if err != nil && !errors.Is(err, etcd.ErrKeyNotFound) {
return nil, false, errors.WithStack(err)
}
if errors.Is(err, etcd.ErrKeyNotFound) {
return nil, false, nil
}
seq := &Sequence{}
if !errors.Is(err, etcd.ErrKeyNotFound) {
kv := resp.Kvs[0]
if err := json.Unmarshal(kv.Value, &seq); err != nil {
return nil, false, errors.WithStack(err)
}
}
return seq, true, nil
}
func IncSequence(ctx context.Context, e *etcd.Store, key string) (*Sequence, error) {
resp, err := e.Get(ctx, key, 0)
if err != nil && !errors.Is(err, etcd.ErrKeyNotFound) {
return nil, errors.WithStack(err)
}
var revision int64
seq := &Sequence{}
if !errors.Is(err, etcd.ErrKeyNotFound) {
kv := resp.Kvs[0]
if err := json.Unmarshal(kv.Value, &seq); err != nil {
return nil, errors.WithStack(err)
}
revision = kv.ModRevision
}
// if the epoch is zero then this is a new etcd cluster. This will happen on
// first creation or if the etcd cluster is recreated. The epoch is used to
// keep the seq incremental and we assume the new epoch will always be greater
// than the previous ones (it not then there was a big backward time drift)
//
// TODO(sgotti) check that the new epoch is greater then previous ones???
if seq.Epoch == 0 {
seq.Epoch = uint64(time.Now().Unix())
}
seq.C++
seqj, err := json.Marshal(seq)
if err != nil {
return nil, errors.WithStack(err)
}
_, err = e.AtomicPut(ctx, key, seqj, revision, nil)
if err != nil {
return nil, errors.WithStack(err)
}
return seq, nil
}