Add sync => go-sync
This commit is contained in:
parent
4035ab5c7b
commit
95d134b57e
141
sync/README.md
Normal file
141
sync/README.md
Normal file
@ -0,0 +1,141 @@
|
||||
# Sync
|
||||
|
||||
Sync is a synchronization library for distributed systems.
|
||||
|
||||
## Overview
|
||||
|
||||
Distributed systems by their very nature are decoupled and independent. In most cases they must honour 2 out of 3 letters of the CAP theorem
|
||||
e.g Availability and Partitional tolerance but sacrificing consistency. In the case of microservices we often offload this concern to
|
||||
an external database or eventing system. Go Sync provides a framework for synchronization which can be used in the application by the developer.
|
||||
|
||||
## Getting Started
|
||||
|
||||
- [Data](#data) - simple distributed data storage
|
||||
- [Leader](#leader) - leadership election for group coordination
|
||||
- [Lock](#lock) - distributed locking for exclusive resource access
|
||||
- [Task](#task) - distributed job execution
|
||||
- [Time](#time) - provides synchronized time
|
||||
|
||||
## Lock
|
||||
|
||||
The Lock interface provides distributed locking. Multiple instances attempting to lock the same id will block until available.
|
||||
|
||||
```go
|
||||
import "github.com/micro/go-micro/sync/lock/consul"
|
||||
|
||||
lock := consul.NewLock()
|
||||
|
||||
// acquire lock
|
||||
err := lock.Acquire("id")
|
||||
// handle err
|
||||
|
||||
// release lock
|
||||
err = lock.Release("id")
|
||||
// handle err
|
||||
```
|
||||
|
||||
## Leader
|
||||
|
||||
Leader provides leadership election. Useful where one node needs to coordinate some action.
|
||||
|
||||
```go
|
||||
import (
|
||||
"github.com/micro/go-micro/sync/leader"
|
||||
"github.com/micro/go-micro/sync/leader/consul"
|
||||
)
|
||||
|
||||
l := consul.NewLeader(
|
||||
leader.Group("name"),
|
||||
)
|
||||
|
||||
// elect leader
|
||||
e, err := l.Elect("id")
|
||||
// handle err
|
||||
|
||||
|
||||
// operate while leader
|
||||
revoked := e.Revoked()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-revoked:
|
||||
// re-elect
|
||||
e.Elect("id")
|
||||
default:
|
||||
// leader operation
|
||||
}
|
||||
}
|
||||
|
||||
// resign leadership
|
||||
e.Resign()
|
||||
```
|
||||
|
||||
## Data
|
||||
|
||||
Data provides a simple interface for distributed data storage.
|
||||
|
||||
```go
|
||||
import (
|
||||
"github.com/micro/go-micro/sync/data"
|
||||
"github.com/micro/go-micro/sync/data/consul"
|
||||
)
|
||||
|
||||
keyval := consul.NewData()
|
||||
|
||||
err := keyval.Write(&data.Record{
|
||||
Key: "foo",
|
||||
Value: []byte(`bar`),
|
||||
})
|
||||
// handle err
|
||||
|
||||
v, err := keyval.Read("foo")
|
||||
// handle err
|
||||
|
||||
err = keyval.Delete("foo")
|
||||
```
|
||||
|
||||
## Task
|
||||
|
||||
Task provides distributed job execution. It's a simple way to distribute work across a coordinated pool of workers.
|
||||
|
||||
```go
|
||||
import (
|
||||
"github.com/micro/go-micro/sync/task"
|
||||
"github.com/micro/go-micro/sync/task/local"
|
||||
)
|
||||
|
||||
t := local.NewTask(
|
||||
task.WithPool(10),
|
||||
)
|
||||
|
||||
err := t.Run(task.Command{
|
||||
Name: "atask",
|
||||
Func: func() error {
|
||||
// exec some work
|
||||
return nil
|
||||
},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
// do something
|
||||
}
|
||||
```
|
||||
|
||||
## Time
|
||||
|
||||
Time provides synchronized time. Local machines may have clock skew and time cannot be guaranteed to be the same everywhere.
|
||||
Synchronized Time allows you to decide how time is defined for your applications.
|
||||
|
||||
```go
|
||||
import (
|
||||
"github.com/micro/go-micro/sync/time/ntp"
|
||||
)
|
||||
|
||||
|
||||
t := ntp.NewTime()
|
||||
time, err := t.Now()
|
||||
```
|
||||
|
||||
## TODO
|
||||
|
||||
- Event package - strongly consistent event stream e.g kafka
|
93
sync/cron.go
Normal file
93
sync/cron.go
Normal file
@ -0,0 +1,93 @@
|
||||
package sync
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"github.com/micro/go-log"
|
||||
"github.com/micro/go-micro/sync/leader/consul"
|
||||
"github.com/micro/go-micro/sync/task"
|
||||
"github.com/micro/go-micro/sync/task/local"
|
||||
)
|
||||
|
||||
type syncCron struct {
|
||||
opts Options
|
||||
}
|
||||
|
||||
func backoff(attempts int) time.Duration {
|
||||
if attempts == 0 {
|
||||
return time.Duration(0)
|
||||
}
|
||||
return time.Duration(math.Pow(10, float64(attempts))) * time.Millisecond
|
||||
}
|
||||
|
||||
func (c *syncCron) Schedule(s task.Schedule, t task.Command) error {
|
||||
id := fmt.Sprintf("%s-%s", s.String(), t.String())
|
||||
|
||||
go func() {
|
||||
// run the scheduler
|
||||
tc := s.Run()
|
||||
|
||||
var i int
|
||||
|
||||
for {
|
||||
// leader election
|
||||
e, err := c.opts.Leader.Elect(id)
|
||||
if err != nil {
|
||||
log.Logf("[cron] leader election error: %v", err)
|
||||
time.Sleep(backoff(i))
|
||||
i++
|
||||
continue
|
||||
}
|
||||
|
||||
i = 0
|
||||
r := e.Revoked()
|
||||
|
||||
// execute the task
|
||||
Tick:
|
||||
for {
|
||||
select {
|
||||
// schedule tick
|
||||
case _, ok := <-tc:
|
||||
// ticked once
|
||||
if !ok {
|
||||
break Tick
|
||||
}
|
||||
|
||||
log.Logf("[cron] executing command %s", t.Name)
|
||||
if err := c.opts.Task.Run(t); err != nil {
|
||||
log.Logf("[cron] error executing command %s: %v", t.Name, err)
|
||||
}
|
||||
// leader revoked
|
||||
case <-r:
|
||||
break Tick
|
||||
}
|
||||
}
|
||||
|
||||
// resign
|
||||
e.Resign()
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewCron(opts ...Option) Cron {
|
||||
var options Options
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
|
||||
if options.Leader == nil {
|
||||
options.Leader = consul.NewLeader()
|
||||
}
|
||||
|
||||
if options.Task == nil {
|
||||
options.Task = local.NewTask()
|
||||
}
|
||||
|
||||
return &syncCron{
|
||||
opts: options,
|
||||
}
|
||||
}
|
93
sync/data/consul/consul.go
Normal file
93
sync/data/consul/consul.go
Normal file
@ -0,0 +1,93 @@
|
||||
// Package consul is a consul implementation of kv
|
||||
package consul
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/hashicorp/consul/api"
|
||||
"github.com/micro/go-micro/sync/data"
|
||||
)
|
||||
|
||||
type ckv struct {
|
||||
client *api.Client
|
||||
}
|
||||
|
||||
func (c *ckv) Read(key string) (*data.Record, error) {
|
||||
keyval, _, err := c.client.KV().Get(key, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if keyval == nil {
|
||||
return nil, data.ErrNotFound
|
||||
}
|
||||
|
||||
return &data.Record{
|
||||
Key: keyval.Key,
|
||||
Value: keyval.Value,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *ckv) Delete(key string) error {
|
||||
_, err := c.client.KV().Delete(key, nil)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *ckv) Write(record *data.Record) error {
|
||||
_, err := c.client.KV().Put(&api.KVPair{
|
||||
Key: record.Key,
|
||||
Value: record.Value,
|
||||
}, nil)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *ckv) Dump() ([]*data.Record, error) {
|
||||
keyval, _, err := c.client.KV().List("/", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if keyval == nil {
|
||||
return nil, data.ErrNotFound
|
||||
}
|
||||
var vals []*data.Record
|
||||
for _, keyv := range keyval {
|
||||
vals = append(vals, &data.Record{
|
||||
Key: keyv.Key,
|
||||
Value: keyv.Value,
|
||||
})
|
||||
}
|
||||
return vals, nil
|
||||
}
|
||||
|
||||
func (c *ckv) String() string {
|
||||
return "consul"
|
||||
}
|
||||
|
||||
func NewData(opts ...data.Option) data.Data {
|
||||
var options data.Options
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
|
||||
config := api.DefaultConfig()
|
||||
|
||||
// set host
|
||||
// config.Host something
|
||||
// check if there are any addrs
|
||||
if len(options.Nodes) > 0 {
|
||||
addr, port, err := net.SplitHostPort(options.Nodes[0])
|
||||
if ae, ok := err.(*net.AddrError); ok && ae.Err == "missing port in address" {
|
||||
port = "8500"
|
||||
config.Address = fmt.Sprintf("%s:%s", options.Nodes[0], port)
|
||||
} else if err == nil {
|
||||
config.Address = fmt.Sprintf("%s:%s", addr, port)
|
||||
}
|
||||
}
|
||||
|
||||
client, _ := api.NewClient(config)
|
||||
|
||||
return &ckv{
|
||||
client: client,
|
||||
}
|
||||
}
|
32
sync/data/data.go
Normal file
32
sync/data/data.go
Normal file
@ -0,0 +1,32 @@
|
||||
// Package data is an interface for key-value storage.
|
||||
package data
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNotFound = errors.New("not found")
|
||||
)
|
||||
|
||||
// Data is a data storage interface
|
||||
type Data interface {
|
||||
// Dump the known records
|
||||
Dump() ([]*Record, error)
|
||||
// Read a record with key
|
||||
Read(key string) (*Record, error)
|
||||
// Write a record
|
||||
Write(r *Record) error
|
||||
// Delete a record with key
|
||||
Delete(key string) error
|
||||
}
|
||||
|
||||
// Record represents a data record
|
||||
type Record struct {
|
||||
Key string
|
||||
Value []byte
|
||||
Expiration time.Duration
|
||||
}
|
||||
|
||||
type Option func(o *Options)
|
93
sync/data/etcd/etcd.go
Normal file
93
sync/data/etcd/etcd.go
Normal file
@ -0,0 +1,93 @@
|
||||
// Package etcd is an etcd v3 implementation of kv
|
||||
package etcd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
|
||||
"github.com/micro/go-micro/sync/data"
|
||||
client "go.etcd.io/etcd/clientv3"
|
||||
)
|
||||
|
||||
type ekv struct {
|
||||
kv client.KV
|
||||
}
|
||||
|
||||
func (e *ekv) Read(key string) (*data.Record, error) {
|
||||
keyval, err := e.kv.Get(context.Background(), key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if keyval == nil || len(keyval.Kvs) == 0 {
|
||||
return nil, data.ErrNotFound
|
||||
}
|
||||
|
||||
return &data.Record{
|
||||
Key: string(keyval.Kvs[0].Key),
|
||||
Value: keyval.Kvs[0].Value,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (e *ekv) Delete(key string) error {
|
||||
_, err := e.kv.Delete(context.Background(), key)
|
||||
return err
|
||||
}
|
||||
|
||||
func (e *ekv) Write(record *data.Record) error {
|
||||
_, err := e.kv.Put(context.Background(), record.Key, string(record.Value))
|
||||
return err
|
||||
}
|
||||
|
||||
func (e *ekv) Dump() ([]*data.Record, error) {
|
||||
keyval, err := e.kv.Get(context.Background(), "/", client.WithPrefix())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var vals []*data.Record
|
||||
if keyval == nil || len(keyval.Kvs) == 0 {
|
||||
return vals, nil
|
||||
}
|
||||
for _, keyv := range keyval.Kvs {
|
||||
vals = append(vals, &data.Record{
|
||||
Key: string(keyv.Key),
|
||||
Value: keyv.Value,
|
||||
})
|
||||
}
|
||||
return vals, nil
|
||||
}
|
||||
|
||||
func (e *ekv) String() string {
|
||||
return "etcd"
|
||||
}
|
||||
|
||||
func NewData(opts ...data.Option) data.Data {
|
||||
var options data.Options
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
|
||||
var endpoints []string
|
||||
|
||||
for _, addr := range options.Nodes {
|
||||
if len(addr) > 0 {
|
||||
endpoints = append(endpoints, addr)
|
||||
}
|
||||
}
|
||||
|
||||
if len(endpoints) == 0 {
|
||||
endpoints = []string{"http://127.0.0.1:2379"}
|
||||
}
|
||||
|
||||
// TODO: parse addresses
|
||||
c, err := client.New(client.Config{
|
||||
Endpoints: endpoints,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
return &ekv{
|
||||
kv: client.NewKV(c),
|
||||
}
|
||||
}
|
178
sync/data/memcached/memcached.go
Normal file
178
sync/data/memcached/memcached.go
Normal file
@ -0,0 +1,178 @@
|
||||
package memcached
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
mc "github.com/bradfitz/gomemcache/memcache"
|
||||
"github.com/micro/go-micro/sync/data"
|
||||
)
|
||||
|
||||
type mkv struct {
|
||||
Server *mc.ServerList
|
||||
Client *mc.Client
|
||||
}
|
||||
|
||||
func (m *mkv) Read(key string) (*data.Record, error) {
|
||||
keyval, err := m.Client.Get(key)
|
||||
if err != nil && err == mc.ErrCacheMiss {
|
||||
return nil, data.ErrNotFound
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if keyval == nil {
|
||||
return nil, data.ErrNotFound
|
||||
}
|
||||
|
||||
return &data.Record{
|
||||
Key: keyval.Key,
|
||||
Value: keyval.Value,
|
||||
Expiration: time.Second * time.Duration(keyval.Expiration),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *mkv) Delete(key string) error {
|
||||
return m.Client.Delete(key)
|
||||
}
|
||||
|
||||
func (m *mkv) Write(record *data.Record) error {
|
||||
return m.Client.Set(&mc.Item{
|
||||
Key: record.Key,
|
||||
Value: record.Value,
|
||||
Expiration: int32(record.Expiration.Seconds()),
|
||||
})
|
||||
}
|
||||
|
||||
func (m *mkv) Dump() ([]*data.Record, error) {
|
||||
// stats
|
||||
// cachedump
|
||||
// get keys
|
||||
|
||||
var keys []string
|
||||
|
||||
//data := make(map[string]string)
|
||||
if err := m.Server.Each(func(c net.Addr) error {
|
||||
cc, err := net.Dial("tcp", c.String())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cc.Close()
|
||||
|
||||
b := bufio.NewReadWriter(bufio.NewReader(cc), bufio.NewWriter(cc))
|
||||
|
||||
// get records
|
||||
if _, err := fmt.Fprintf(b, "stats records\r\n"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b.Flush()
|
||||
|
||||
v, err := b.ReadSlice('\n')
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
parts := bytes.Split(v, []byte("\n"))
|
||||
if len(parts) < 1 {
|
||||
return nil
|
||||
}
|
||||
vals := strings.Split(string(parts[0]), ":")
|
||||
records := vals[1]
|
||||
|
||||
// drain
|
||||
for {
|
||||
buf, err := b.ReadSlice('\n')
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if strings.HasPrefix(string(buf), "END") {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
b.Writer.Reset(cc)
|
||||
b.Reader.Reset(cc)
|
||||
|
||||
if _, err := fmt.Fprintf(b, "lru_crawler metadump %s\r\n", records); err != nil {
|
||||
return err
|
||||
}
|
||||
b.Flush()
|
||||
|
||||
for {
|
||||
v, err := b.ReadString('\n')
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if strings.HasPrefix(v, "END") {
|
||||
break
|
||||
}
|
||||
key := strings.Split(v, " ")[0]
|
||||
keys = append(keys, strings.TrimPrefix(key, "key="))
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var vals []*data.Record
|
||||
|
||||
// concurrent op
|
||||
ch := make(chan *data.Record, len(keys))
|
||||
|
||||
for _, k := range keys {
|
||||
go func(key string) {
|
||||
i, _ := m.Read(key)
|
||||
ch <- i
|
||||
}(k)
|
||||
}
|
||||
|
||||
for i := 0; i < len(keys); i++ {
|
||||
record := <-ch
|
||||
|
||||
if record == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
vals = append(vals, record)
|
||||
}
|
||||
|
||||
close(ch)
|
||||
|
||||
return vals, nil
|
||||
}
|
||||
|
||||
func (m *mkv) String() string {
|
||||
return "memcached"
|
||||
}
|
||||
|
||||
func NewData(opts ...data.Option) data.Data {
|
||||
var options data.Options
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
|
||||
if len(options.Nodes) == 0 {
|
||||
options.Nodes = []string{"127.0.0.1:11211"}
|
||||
}
|
||||
|
||||
ss := new(mc.ServerList)
|
||||
ss.SetServers(options.Nodes...)
|
||||
|
||||
return &mkv{
|
||||
Server: ss,
|
||||
Client: mc.New(options.Nodes...),
|
||||
}
|
||||
}
|
19
sync/data/options.go
Normal file
19
sync/data/options.go
Normal file
@ -0,0 +1,19 @@
|
||||
package data
|
||||
|
||||
type Options struct {
|
||||
Nodes []string
|
||||
Prefix string
|
||||
}
|
||||
|
||||
func Nodes(a ...string) Option {
|
||||
return func(o *Options) {
|
||||
o.Nodes = a
|
||||
}
|
||||
}
|
||||
|
||||
// Prefix sets a prefix to any lock ids used
|
||||
func Prefix(p string) Option {
|
||||
return func(o *Options) {
|
||||
o.Prefix = p
|
||||
}
|
||||
}
|
82
sync/data/redis/redis.go
Normal file
82
sync/data/redis/redis.go
Normal file
@ -0,0 +1,82 @@
|
||||
package redis
|
||||
|
||||
import (
|
||||
"github.com/micro/go-micro/sync/data"
|
||||
redis "gopkg.in/redis.v3"
|
||||
)
|
||||
|
||||
type rkv struct {
|
||||
Client *redis.Client
|
||||
}
|
||||
|
||||
func (r *rkv) Read(key string) (*data.Record, error) {
|
||||
val, err := r.Client.Get(key).Bytes()
|
||||
|
||||
if err != nil && err == redis.Nil {
|
||||
return nil, data.ErrNotFound
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if val == nil {
|
||||
return nil, data.ErrNotFound
|
||||
}
|
||||
|
||||
d, err := r.Client.TTL(key).Result()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &data.Record{
|
||||
Key: key,
|
||||
Value: val,
|
||||
Expiration: d,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *rkv) Delete(key string) error {
|
||||
return r.Client.Del(key).Err()
|
||||
}
|
||||
|
||||
func (r *rkv) Write(record *data.Record) error {
|
||||
return r.Client.Set(record.Key, record.Value, record.Expiration).Err()
|
||||
}
|
||||
|
||||
func (r *rkv) Dump() ([]*data.Record, error) {
|
||||
keys, err := r.Client.Keys("*").Result()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var vals []*data.Record
|
||||
for _, k := range keys {
|
||||
i, err := r.Read(k)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
vals = append(vals, i)
|
||||
}
|
||||
return vals, nil
|
||||
}
|
||||
|
||||
func (r *rkv) String() string {
|
||||
return "redis"
|
||||
}
|
||||
|
||||
func NewData(opts ...data.Option) data.Data {
|
||||
var options data.Options
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
|
||||
if len(options.Nodes) == 0 {
|
||||
options.Nodes = []string{"127.0.0.1:6379"}
|
||||
}
|
||||
|
||||
return &rkv{
|
||||
Client: redis.NewClient(&redis.Options{
|
||||
Addr: options.Nodes[0],
|
||||
Password: "", // no password set
|
||||
DB: 0, // use default DB
|
||||
}),
|
||||
}
|
||||
}
|
157
sync/db.go
Normal file
157
sync/db.go
Normal file
@ -0,0 +1,157 @@
|
||||
package sync
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/micro/go-micro/sync/data"
|
||||
ckv "github.com/micro/go-micro/sync/data/consul"
|
||||
lock "github.com/micro/go-micro/sync/lock/consul"
|
||||
)
|
||||
|
||||
type syncDB struct {
|
||||
opts Options
|
||||
}
|
||||
|
||||
func ekey(k interface{}) string {
|
||||
b, _ := json.Marshal(k)
|
||||
return base64.StdEncoding.EncodeToString(b)
|
||||
}
|
||||
|
||||
func (m *syncDB) Read(key, val interface{}) error {
|
||||
if key == nil {
|
||||
return fmt.Errorf("key is nil")
|
||||
}
|
||||
|
||||
kstr := ekey(key)
|
||||
|
||||
// lock
|
||||
if err := m.opts.Lock.Acquire(kstr); err != nil {
|
||||
return err
|
||||
}
|
||||
defer m.opts.Lock.Release(kstr)
|
||||
|
||||
// get key
|
||||
kval, err := m.opts.Data.Read(kstr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// decode value
|
||||
return json.Unmarshal(kval.Value, val)
|
||||
}
|
||||
|
||||
func (m *syncDB) Write(key, val interface{}) error {
|
||||
if key == nil {
|
||||
return fmt.Errorf("key is nil")
|
||||
}
|
||||
|
||||
kstr := ekey(key)
|
||||
|
||||
// lock
|
||||
if err := m.opts.Lock.Acquire(kstr); err != nil {
|
||||
return err
|
||||
}
|
||||
defer m.opts.Lock.Release(kstr)
|
||||
|
||||
// encode value
|
||||
b, err := json.Marshal(val)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// set key
|
||||
return m.opts.Data.Write(&data.Record{
|
||||
Key: kstr,
|
||||
Value: b,
|
||||
})
|
||||
}
|
||||
|
||||
func (m *syncDB) Delete(key interface{}) error {
|
||||
if key == nil {
|
||||
return fmt.Errorf("key is nil")
|
||||
}
|
||||
|
||||
kstr := ekey(key)
|
||||
|
||||
// lock
|
||||
if err := m.opts.Lock.Acquire(kstr); err != nil {
|
||||
return err
|
||||
}
|
||||
defer m.opts.Lock.Release(kstr)
|
||||
return m.opts.Data.Delete(kstr)
|
||||
}
|
||||
|
||||
func (m *syncDB) Iterate(fn func(key, val interface{}) error) error {
|
||||
keyvals, err := m.opts.Data.Dump()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, keyval := range keyvals {
|
||||
// lock
|
||||
if err := m.opts.Lock.Acquire(keyval.Key); err != nil {
|
||||
return err
|
||||
}
|
||||
// unlock
|
||||
defer m.opts.Lock.Release(keyval.Key)
|
||||
|
||||
// unmarshal value
|
||||
var val interface{}
|
||||
|
||||
if len(keyval.Value) > 0 && keyval.Value[0] == '{' {
|
||||
if err := json.Unmarshal(keyval.Value, &val); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
val = keyval.Value
|
||||
}
|
||||
|
||||
// exec func
|
||||
if err := fn(keyval.Key, val); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// save val
|
||||
b, err := json.Marshal(val)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// no save
|
||||
if i := bytes.Compare(keyval.Value, b); i == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// set key
|
||||
if err := m.opts.Data.Write(&data.Record{
|
||||
Key: keyval.Key,
|
||||
Value: b,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewDB(opts ...Option) DB {
|
||||
var options Options
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
|
||||
if options.Lock == nil {
|
||||
options.Lock = lock.NewLock()
|
||||
}
|
||||
|
||||
if options.Data == nil {
|
||||
options.Data = ckv.NewData()
|
||||
}
|
||||
|
||||
return &syncDB{
|
||||
opts: options,
|
||||
}
|
||||
}
|
27
sync/event/event.go
Normal file
27
sync/event/event.go
Normal file
@ -0,0 +1,27 @@
|
||||
// Package event provides a distributed log interface
|
||||
package event
|
||||
|
||||
// Event provides a distributed log interface
|
||||
type Event interface {
|
||||
// Log retrieves the log with an id/name
|
||||
Log(id string) (Log, error)
|
||||
}
|
||||
|
||||
// Log is an individual event log
|
||||
type Log interface {
|
||||
// Close the log handle
|
||||
Close() error
|
||||
// Log ID
|
||||
Id() string
|
||||
// Read will read the next record
|
||||
Read() (*Record, error)
|
||||
// Go to an offset
|
||||
Seek(offset int64) error
|
||||
// Write an event to the log
|
||||
Write(*Record) error
|
||||
}
|
||||
|
||||
type Record struct {
|
||||
Metadata map[string]interface{}
|
||||
Data []byte
|
||||
}
|
158
sync/leader/consul/consul.go
Normal file
158
sync/leader/consul/consul.go
Normal file
@ -0,0 +1,158 @@
|
||||
package consul
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"path"
|
||||
"sync"
|
||||
|
||||
"github.com/hashicorp/consul/api"
|
||||
"github.com/hashicorp/consul/api/watch"
|
||||
"github.com/micro/go-micro/sync/leader"
|
||||
)
|
||||
|
||||
type consulLeader struct {
|
||||
opts leader.Options
|
||||
c *api.Client
|
||||
}
|
||||
|
||||
type consulElected struct {
|
||||
c *api.Client
|
||||
l *api.Lock
|
||||
id string
|
||||
key string
|
||||
opts leader.ElectOptions
|
||||
|
||||
mtx sync.RWMutex
|
||||
rv <-chan struct{}
|
||||
}
|
||||
|
||||
func (c *consulLeader) Elect(id string, opts ...leader.ElectOption) (leader.Elected, error) {
|
||||
var options leader.ElectOptions
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
|
||||
key := path.Join("micro/leader", c.opts.Group)
|
||||
|
||||
lc, err := c.c.LockOpts(&api.LockOptions{
|
||||
Key: key,
|
||||
Value: []byte(id),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rv, err := lc.Lock(nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &consulElected{
|
||||
c: c.c,
|
||||
key: key,
|
||||
rv: rv,
|
||||
id: id,
|
||||
l: lc,
|
||||
opts: options,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *consulLeader) Follow() chan string {
|
||||
ch := make(chan string, 1)
|
||||
|
||||
key := path.Join("/micro/leader", c.opts.Group)
|
||||
|
||||
p, err := watch.Parse(map[string]interface{}{
|
||||
"type": "key",
|
||||
"key": key,
|
||||
})
|
||||
if err != nil {
|
||||
return ch
|
||||
}
|
||||
p.Handler = func(idx uint64, raw interface{}) {
|
||||
if raw == nil {
|
||||
return // ignore
|
||||
}
|
||||
v, ok := raw.(*api.KVPair)
|
||||
if !ok || v == nil {
|
||||
return // ignore
|
||||
}
|
||||
ch <- string(v.Value)
|
||||
}
|
||||
|
||||
go p.RunWithClientAndLogger(c.c, log.New(os.Stdout, "consul: ", log.Lshortfile))
|
||||
return ch
|
||||
}
|
||||
|
||||
func (c *consulLeader) String() string {
|
||||
return "consul"
|
||||
}
|
||||
|
||||
func (c *consulElected) Id() string {
|
||||
return c.id
|
||||
}
|
||||
|
||||
func (c *consulElected) Reelect() error {
|
||||
rv, err := c.l.Lock(nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.mtx.Lock()
|
||||
c.rv = rv
|
||||
c.mtx.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *consulElected) Revoked() chan bool {
|
||||
ch := make(chan bool, 1)
|
||||
c.mtx.RLock()
|
||||
rv := c.rv
|
||||
c.mtx.RUnlock()
|
||||
|
||||
go func() {
|
||||
<-rv
|
||||
ch <- true
|
||||
close(ch)
|
||||
}()
|
||||
|
||||
return ch
|
||||
}
|
||||
|
||||
func (c *consulElected) Resign() error {
|
||||
return c.l.Unlock()
|
||||
}
|
||||
|
||||
func NewLeader(opts ...leader.Option) leader.Leader {
|
||||
options := leader.Options{
|
||||
Group: "default",
|
||||
}
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
|
||||
config := api.DefaultConfig()
|
||||
|
||||
// set host
|
||||
// config.Host something
|
||||
// check if there are any addrs
|
||||
if len(options.Nodes) > 0 {
|
||||
addr, port, err := net.SplitHostPort(options.Nodes[0])
|
||||
if ae, ok := err.(*net.AddrError); ok && ae.Err == "missing port in address" {
|
||||
port = "8500"
|
||||
config.Address = fmt.Sprintf("%s:%s", addr, port)
|
||||
} else if err == nil {
|
||||
config.Address = fmt.Sprintf("%s:%s", addr, port)
|
||||
}
|
||||
}
|
||||
|
||||
client, _ := api.NewClient(config)
|
||||
|
||||
return &consulLeader{
|
||||
opts: options,
|
||||
c: client,
|
||||
}
|
||||
}
|
145
sync/leader/etcd/etcd.go
Normal file
145
sync/leader/etcd/etcd.go
Normal file
@ -0,0 +1,145 @@
|
||||
package etcd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/micro/go-micro/sync/leader"
|
||||
client "go.etcd.io/etcd/clientv3"
|
||||
cc "go.etcd.io/etcd/clientv3/concurrency"
|
||||
)
|
||||
|
||||
type etcdLeader struct {
|
||||
opts leader.Options
|
||||
path string
|
||||
client *client.Client
|
||||
}
|
||||
|
||||
type etcdElected struct {
|
||||
s *cc.Session
|
||||
e *cc.Election
|
||||
id string
|
||||
}
|
||||
|
||||
func (e *etcdLeader) Elect(id string, opts ...leader.ElectOption) (leader.Elected, error) {
|
||||
var options leader.ElectOptions
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
|
||||
// make path
|
||||
path := path.Join(e.path, strings.Replace(id, "/", "-", -1))
|
||||
|
||||
s, err := cc.NewSession(e.client)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
l := cc.NewElection(s, path)
|
||||
|
||||
ctx, _ := context.WithCancel(context.Background())
|
||||
|
||||
if err := l.Campaign(ctx, id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &etcdElected{
|
||||
e: l,
|
||||
id: id,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (e *etcdLeader) Follow() chan string {
|
||||
ch := make(chan string)
|
||||
|
||||
s, err := cc.NewSession(e.client)
|
||||
if err != nil {
|
||||
return ch
|
||||
}
|
||||
|
||||
l := cc.NewElection(s, e.path)
|
||||
ech := l.Observe(context.Background())
|
||||
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case r, ok := <-ech:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
ch <- string(r.Kvs[0].Value)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return ch
|
||||
}
|
||||
|
||||
func (e *etcdLeader) String() string {
|
||||
return "etcd"
|
||||
}
|
||||
|
||||
func (e *etcdElected) Reelect() error {
|
||||
ctx, _ := context.WithCancel(context.Background())
|
||||
return e.e.Campaign(ctx, e.id)
|
||||
}
|
||||
|
||||
func (e *etcdElected) Revoked() chan bool {
|
||||
ch := make(chan bool, 1)
|
||||
ech := e.e.Observe(context.Background())
|
||||
|
||||
go func() {
|
||||
for r := range ech {
|
||||
if string(r.Kvs[0].Value) != e.id {
|
||||
ch <- true
|
||||
close(ch)
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return ch
|
||||
}
|
||||
|
||||
func (e *etcdElected) Resign() error {
|
||||
return e.e.Resign(context.Background())
|
||||
}
|
||||
|
||||
func (e *etcdElected) Id() string {
|
||||
return e.id
|
||||
}
|
||||
|
||||
func NewLeader(opts ...leader.Option) leader.Leader {
|
||||
var options leader.Options
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
|
||||
var endpoints []string
|
||||
|
||||
for _, addr := range options.Nodes {
|
||||
if len(addr) > 0 {
|
||||
endpoints = append(endpoints, addr)
|
||||
}
|
||||
}
|
||||
|
||||
if len(endpoints) == 0 {
|
||||
endpoints = []string{"http://127.0.0.1:2379"}
|
||||
}
|
||||
|
||||
// TODO: parse addresses
|
||||
c, err := client.New(client.Config{
|
||||
Endpoints: endpoints,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
return &etcdLeader{
|
||||
path: "/micro/leader",
|
||||
client: c,
|
||||
opts: options,
|
||||
}
|
||||
}
|
25
sync/leader/leader.go
Normal file
25
sync/leader/leader.go
Normal file
@ -0,0 +1,25 @@
|
||||
// Package leader provides leader election
|
||||
package leader
|
||||
|
||||
// Leader provides leadership election
|
||||
type Leader interface {
|
||||
// elect leader
|
||||
Elect(id string, opts ...ElectOption) (Elected, error)
|
||||
// follow the leader
|
||||
Follow() chan string
|
||||
}
|
||||
|
||||
type Elected interface {
|
||||
// id of leader
|
||||
Id() string
|
||||
// seek re-election
|
||||
Reelect() error
|
||||
// resign leadership
|
||||
Resign() error
|
||||
// observe leadership revocation
|
||||
Revoked() chan bool
|
||||
}
|
||||
|
||||
type Option func(o *Options)
|
||||
|
||||
type ElectOption func(o *ElectOptions)
|
22
sync/leader/options.go
Normal file
22
sync/leader/options.go
Normal file
@ -0,0 +1,22 @@
|
||||
package leader
|
||||
|
||||
type Options struct {
|
||||
Nodes []string
|
||||
Group string
|
||||
}
|
||||
|
||||
type ElectOptions struct{}
|
||||
|
||||
// Nodes sets the addresses of the underlying systems
|
||||
func Nodes(a ...string) Option {
|
||||
return func(o *Options) {
|
||||
o.Nodes = a
|
||||
}
|
||||
}
|
||||
|
||||
// Group sets the group name for coordinating leadership
|
||||
func Group(g string) Option {
|
||||
return func(o *Options) {
|
||||
o.Group = g
|
||||
}
|
||||
}
|
104
sync/lock/consul/consul.go
Normal file
104
sync/lock/consul/consul.go
Normal file
@ -0,0 +1,104 @@
|
||||
// Package consul is a consul implemenation of lock
|
||||
package consul
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/consul/api"
|
||||
lock "github.com/micro/go-micro/sync/lock"
|
||||
)
|
||||
|
||||
type consulLock struct {
|
||||
sync.Mutex
|
||||
|
||||
locks map[string]*api.Lock
|
||||
opts lock.Options
|
||||
c *api.Client
|
||||
}
|
||||
|
||||
func (c *consulLock) Acquire(id string, opts ...lock.AcquireOption) error {
|
||||
var options lock.AcquireOptions
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
|
||||
if options.Wait <= time.Duration(0) {
|
||||
options.Wait = api.DefaultLockWaitTime
|
||||
}
|
||||
|
||||
ttl := fmt.Sprintf("%v", options.TTL)
|
||||
if options.TTL <= time.Duration(0) {
|
||||
ttl = api.DefaultLockSessionTTL
|
||||
}
|
||||
|
||||
l, err := c.c.LockOpts(&api.LockOptions{
|
||||
Key: c.opts.Prefix + id,
|
||||
LockWaitTime: options.Wait,
|
||||
SessionTTL: ttl,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = l.Lock(nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.Lock()
|
||||
c.locks[id] = l
|
||||
c.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *consulLock) Release(id string) error {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
l, ok := c.locks[id]
|
||||
if !ok {
|
||||
return errors.New("lock not found")
|
||||
}
|
||||
err := l.Unlock()
|
||||
delete(c.locks, id)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *consulLock) String() string {
|
||||
return "consul"
|
||||
}
|
||||
|
||||
func NewLock(opts ...lock.Option) lock.Lock {
|
||||
var options lock.Options
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
|
||||
config := api.DefaultConfig()
|
||||
|
||||
// set host
|
||||
// config.Host something
|
||||
// check if there are any addrs
|
||||
if len(options.Nodes) > 0 {
|
||||
addr, port, err := net.SplitHostPort(options.Nodes[0])
|
||||
if ae, ok := err.(*net.AddrError); ok && ae.Err == "missing port in address" {
|
||||
port = "8500"
|
||||
config.Address = fmt.Sprintf("%s:%s", options.Nodes[0], port)
|
||||
} else if err == nil {
|
||||
config.Address = fmt.Sprintf("%s:%s", addr, port)
|
||||
}
|
||||
}
|
||||
|
||||
client, _ := api.NewClient(config)
|
||||
|
||||
return &consulLock{
|
||||
locks: make(map[string]*api.Lock),
|
||||
opts: options,
|
||||
c: client,
|
||||
}
|
||||
}
|
115
sync/lock/etcd/etcd.go
Normal file
115
sync/lock/etcd/etcd.go
Normal file
@ -0,0 +1,115 @@
|
||||
// Package etcd is an etcd implementation of lock
|
||||
package etcd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"log"
|
||||
"path"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/micro/go-micro/sync/lock"
|
||||
client "go.etcd.io/etcd/clientv3"
|
||||
cc "go.etcd.io/etcd/clientv3/concurrency"
|
||||
)
|
||||
|
||||
type etcdLock struct {
|
||||
opts lock.Options
|
||||
path string
|
||||
client *client.Client
|
||||
|
||||
sync.Mutex
|
||||
locks map[string]*elock
|
||||
}
|
||||
|
||||
type elock struct {
|
||||
s *cc.Session
|
||||
m *cc.Mutex
|
||||
}
|
||||
|
||||
func (e *etcdLock) Acquire(id string, opts ...lock.AcquireOption) error {
|
||||
var options lock.AcquireOptions
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
|
||||
// make path
|
||||
path := path.Join(e.path, strings.Replace(e.opts.Prefix+id, "/", "-", -1))
|
||||
|
||||
var sopts []cc.SessionOption
|
||||
if options.TTL > 0 {
|
||||
sopts = append(sopts, cc.WithTTL(int(options.TTL.Seconds())))
|
||||
}
|
||||
|
||||
s, err := cc.NewSession(e.client, sopts...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m := cc.NewMutex(s, path)
|
||||
|
||||
ctx, _ := context.WithCancel(context.Background())
|
||||
|
||||
if err := m.Lock(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
e.Lock()
|
||||
e.locks[id] = &elock{
|
||||
s: s,
|
||||
m: m,
|
||||
}
|
||||
e.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *etcdLock) Release(id string) error {
|
||||
e.Lock()
|
||||
defer e.Unlock()
|
||||
v, ok := e.locks[id]
|
||||
if !ok {
|
||||
return errors.New("lock not found")
|
||||
}
|
||||
err := v.m.Unlock(context.Background())
|
||||
delete(e.locks, id)
|
||||
return err
|
||||
}
|
||||
|
||||
func (e *etcdLock) String() string {
|
||||
return "etcd"
|
||||
}
|
||||
|
||||
func NewLock(opts ...lock.Option) lock.Lock {
|
||||
var options lock.Options
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
|
||||
var endpoints []string
|
||||
|
||||
for _, addr := range options.Nodes {
|
||||
if len(addr) > 0 {
|
||||
endpoints = append(endpoints, addr)
|
||||
}
|
||||
}
|
||||
|
||||
if len(endpoints) == 0 {
|
||||
endpoints = []string{"http://127.0.0.1:2379"}
|
||||
}
|
||||
|
||||
// TODO: parse addresses
|
||||
c, err := client.New(client.Config{
|
||||
Endpoints: endpoints,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
return &etcdLock{
|
||||
path: "/micro/lock",
|
||||
client: c,
|
||||
opts: options,
|
||||
locks: make(map[string]*elock),
|
||||
}
|
||||
}
|
27
sync/lock/lock.go
Normal file
27
sync/lock/lock.go
Normal file
@ -0,0 +1,27 @@
|
||||
// Package lock provides distributed locking
|
||||
package lock
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Lock is a distributed locking interface
|
||||
type Lock interface {
|
||||
// Acquire a lock with given id
|
||||
Acquire(id string, opts ...AcquireOption) error
|
||||
// Release the lock with given id
|
||||
Release(id string) error
|
||||
}
|
||||
|
||||
type Options struct {
|
||||
Nodes []string
|
||||
Prefix string
|
||||
}
|
||||
|
||||
type AcquireOptions struct {
|
||||
TTL time.Duration
|
||||
Wait time.Duration
|
||||
}
|
||||
|
||||
type Option func(o *Options)
|
||||
type AcquireOption func(o *AcquireOptions)
|
33
sync/lock/options.go
Normal file
33
sync/lock/options.go
Normal file
@ -0,0 +1,33 @@
|
||||
package lock
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Nodes sets the addresses the underlying lock implementation
|
||||
func Nodes(a ...string) Option {
|
||||
return func(o *Options) {
|
||||
o.Nodes = a
|
||||
}
|
||||
}
|
||||
|
||||
// Prefix sets a prefix to any lock ids used
|
||||
func Prefix(p string) Option {
|
||||
return func(o *Options) {
|
||||
o.Prefix = p
|
||||
}
|
||||
}
|
||||
|
||||
// TTL sets the lock ttl
|
||||
func TTL(t time.Duration) AcquireOption {
|
||||
return func(o *AcquireOptions) {
|
||||
o.TTL = t
|
||||
}
|
||||
}
|
||||
|
||||
// Wait sets the wait time
|
||||
func Wait(t time.Duration) AcquireOption {
|
||||
return func(o *AcquireOptions) {
|
||||
o.Wait = t
|
||||
}
|
||||
}
|
29
sync/lock/redis/pool.go
Normal file
29
sync/lock/redis/pool.go
Normal file
@ -0,0 +1,29 @@
|
||||
package redis
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/gomodule/redigo/redis"
|
||||
)
|
||||
|
||||
type pool struct {
|
||||
sync.Mutex
|
||||
i int
|
||||
addrs []string
|
||||
}
|
||||
|
||||
func (p *pool) Get() redis.Conn {
|
||||
for i := 0; i < 3; i++ {
|
||||
p.Lock()
|
||||
addr := p.addrs[p.i%len(p.addrs)]
|
||||
p.i++
|
||||
p.Unlock()
|
||||
|
||||
c, err := redis.Dial("tcp", addr)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
return c
|
||||
}
|
||||
return nil
|
||||
}
|
94
sync/lock/redis/redis.go
Normal file
94
sync/lock/redis/redis.go
Normal file
@ -0,0 +1,94 @@
|
||||
// Package redis is a redis implemenation of lock
|
||||
package redis
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/go-redsync/redsync"
|
||||
"github.com/micro/go-micro/sync/lock"
|
||||
)
|
||||
|
||||
type redisLock struct {
|
||||
sync.Mutex
|
||||
|
||||
locks map[string]*redsync.Mutex
|
||||
opts lock.Options
|
||||
c *redsync.Redsync
|
||||
}
|
||||
|
||||
func (r *redisLock) Acquire(id string, opts ...lock.AcquireOption) error {
|
||||
var options lock.AcquireOptions
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
|
||||
var ropts []redsync.Option
|
||||
|
||||
if options.Wait > time.Duration(0) {
|
||||
ropts = append(ropts, redsync.SetRetryDelay(options.Wait))
|
||||
ropts = append(ropts, redsync.SetTries(1))
|
||||
}
|
||||
|
||||
if options.TTL > time.Duration(0) {
|
||||
ropts = append(ropts, redsync.SetExpiry(options.TTL))
|
||||
}
|
||||
|
||||
m := r.c.NewMutex(r.opts.Prefix+id, ropts...)
|
||||
err := m.Lock()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r.Lock()
|
||||
r.locks[id] = m
|
||||
r.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *redisLock) Release(id string) error {
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
m, ok := r.locks[id]
|
||||
if !ok {
|
||||
return errors.New("lock not found")
|
||||
}
|
||||
|
||||
unlocked := m.Unlock()
|
||||
delete(r.locks, id)
|
||||
|
||||
if !unlocked {
|
||||
return errors.New("lock not unlocked")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *redisLock) String() string {
|
||||
return "redis"
|
||||
}
|
||||
|
||||
func NewLock(opts ...lock.Option) lock.Lock {
|
||||
var options lock.Options
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
|
||||
nodes := options.Nodes
|
||||
|
||||
if len(nodes) == 0 {
|
||||
nodes = []string{"127.0.0.1:6379"}
|
||||
}
|
||||
|
||||
rpool := redsync.New([]redsync.Pool{&pool{
|
||||
addrs: nodes,
|
||||
}})
|
||||
|
||||
return &redisLock{
|
||||
locks: make(map[string]*redsync.Mutex),
|
||||
opts: options,
|
||||
c: rpool,
|
||||
}
|
||||
}
|
36
sync/options.go
Normal file
36
sync/options.go
Normal file
@ -0,0 +1,36 @@
|
||||
package sync
|
||||
|
||||
import (
|
||||
"github.com/micro/go-micro/sync/data"
|
||||
"github.com/micro/go-micro/sync/leader"
|
||||
"github.com/micro/go-micro/sync/lock"
|
||||
"github.com/micro/go-micro/sync/time"
|
||||
)
|
||||
|
||||
// WithLeader sets the leader election implementation opton
|
||||
func WithLeader(l leader.Leader) Option {
|
||||
return func(o *Options) {
|
||||
o.Leader = l
|
||||
}
|
||||
}
|
||||
|
||||
// WithLock sets the locking implementation option
|
||||
func WithLock(l lock.Lock) Option {
|
||||
return func(o *Options) {
|
||||
o.Lock = l
|
||||
}
|
||||
}
|
||||
|
||||
// WithData sets the data implementation option
|
||||
func WithData(s data.Data) Option {
|
||||
return func(o *Options) {
|
||||
o.Data = s
|
||||
}
|
||||
}
|
||||
|
||||
// WithTime sets the time implementation option
|
||||
func WithTime(t time.Time) Option {
|
||||
return func(o *Options) {
|
||||
o.Time = t
|
||||
}
|
||||
}
|
41
sync/sync.go
Normal file
41
sync/sync.go
Normal file
@ -0,0 +1,41 @@
|
||||
// Package sync is a distributed synchronization framework
|
||||
package sync
|
||||
|
||||
import (
|
||||
"github.com/micro/go-micro/sync/data"
|
||||
"github.com/micro/go-micro/sync/leader"
|
||||
"github.com/micro/go-micro/sync/lock"
|
||||
"github.com/micro/go-micro/sync/task"
|
||||
"github.com/micro/go-micro/sync/time"
|
||||
)
|
||||
|
||||
// DB provides synchronized access to key-value storage.
|
||||
// It uses the data interface and lock interface to
|
||||
// provide a consistent storage mechanism.
|
||||
type DB interface {
|
||||
// Read value with given key
|
||||
Read(key, val interface{}) error
|
||||
// Write value with given key
|
||||
Write(key, val interface{}) error
|
||||
// Delete value with given key
|
||||
Delete(key interface{}) error
|
||||
// Iterate over all key/vals. Value changes are saved
|
||||
Iterate(func(key, val interface{}) error) error
|
||||
}
|
||||
|
||||
// Cron is a distributed scheduler using leader election
|
||||
// and distributed task runners. It uses the leader and
|
||||
// task interfaces.
|
||||
type Cron interface {
|
||||
Schedule(task.Schedule, task.Command) error
|
||||
}
|
||||
|
||||
type Options struct {
|
||||
Leader leader.Leader
|
||||
Lock lock.Lock
|
||||
Data data.Data
|
||||
Task task.Task
|
||||
Time time.Time
|
||||
}
|
||||
|
||||
type Option func(o *Options)
|
219
sync/task/broker/broker.go
Normal file
219
sync/task/broker/broker.go
Normal file
@ -0,0 +1,219 @@
|
||||
// Package broker provides a distributed task manager built on the micro broker
|
||||
package broker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/micro/go-micro/broker"
|
||||
"github.com/micro/go-micro/sync/task"
|
||||
)
|
||||
|
||||
type brokerKey struct{}
|
||||
|
||||
// Task is a broker task
|
||||
type Task struct {
|
||||
// a micro broker
|
||||
Broker broker.Broker
|
||||
// Options
|
||||
Options task.Options
|
||||
|
||||
mtx sync.RWMutex
|
||||
status string
|
||||
}
|
||||
|
||||
func returnError(err error, ch chan error) {
|
||||
select {
|
||||
case ch <- err:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Task) Run(c task.Command) error {
|
||||
// connect
|
||||
t.Broker.Connect()
|
||||
// unique id for this runner
|
||||
id := uuid.New().String()
|
||||
// topic of the command
|
||||
topic := fmt.Sprintf("task.%s", c.Name)
|
||||
|
||||
// global error
|
||||
errCh := make(chan error, t.Options.Pool)
|
||||
|
||||
// subscribe for distributed work
|
||||
workFn := func(p broker.Publication) error {
|
||||
msg := p.Message()
|
||||
|
||||
// get command name
|
||||
command := msg.Header["Command"]
|
||||
|
||||
// check the command is what we expect
|
||||
if command != c.Name {
|
||||
returnError(errors.New("received unknown command: "+command), errCh)
|
||||
return nil
|
||||
}
|
||||
|
||||
// new task created
|
||||
switch msg.Header["Status"] {
|
||||
case "start":
|
||||
// artificially delay start of processing
|
||||
time.Sleep(time.Millisecond * time.Duration(10+rand.Intn(100)))
|
||||
|
||||
// execute the function
|
||||
err := c.Func()
|
||||
|
||||
status := "done"
|
||||
errors := ""
|
||||
|
||||
if err != nil {
|
||||
status = "error"
|
||||
errors = err.Error()
|
||||
}
|
||||
|
||||
// create response
|
||||
msg := &broker.Message{
|
||||
Header: map[string]string{
|
||||
"Command": c.Name,
|
||||
"Error": errors,
|
||||
"Id": id,
|
||||
"Status": status,
|
||||
"Timestamp": fmt.Sprintf("%d", time.Now().Unix()),
|
||||
},
|
||||
// Body is nil, may be used in future
|
||||
}
|
||||
|
||||
// publish end of task
|
||||
if err := t.Broker.Publish(topic, msg); err != nil {
|
||||
returnError(err, errCh)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// subscribe for the pool size
|
||||
for i := 0; i < t.Options.Pool; i++ {
|
||||
// subscribe to work
|
||||
subWork, err := t.Broker.Subscribe(topic, workFn, broker.Queue(fmt.Sprintf("work.%d", i)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// unsubscribe on completion
|
||||
defer subWork.Unsubscribe()
|
||||
}
|
||||
|
||||
// subscribe to all status messages
|
||||
subStatus, err := t.Broker.Subscribe(topic, func(p broker.Publication) error {
|
||||
msg := p.Message()
|
||||
|
||||
// get command name
|
||||
command := msg.Header["Command"]
|
||||
|
||||
// check the command is what we expect
|
||||
if command != c.Name {
|
||||
return nil
|
||||
}
|
||||
|
||||
// check task status
|
||||
switch msg.Header["Status"] {
|
||||
// task is complete
|
||||
case "done":
|
||||
errCh <- nil
|
||||
// someone failed
|
||||
case "error":
|
||||
returnError(errors.New(msg.Header["Error"]), errCh)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer subStatus.Unsubscribe()
|
||||
|
||||
// a new task
|
||||
msg := &broker.Message{
|
||||
Header: map[string]string{
|
||||
"Command": c.Name,
|
||||
"Id": id,
|
||||
"Status": "start",
|
||||
"Timestamp": fmt.Sprintf("%d", time.Now().Unix()),
|
||||
},
|
||||
}
|
||||
|
||||
// artificially delay the start of the task
|
||||
time.Sleep(time.Millisecond * time.Duration(10+rand.Intn(100)))
|
||||
|
||||
// publish the task
|
||||
if err := t.Broker.Publish(topic, msg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var gerrors []string
|
||||
|
||||
// wait for all responses
|
||||
for i := 0; i < t.Options.Pool; i++ {
|
||||
// check errors
|
||||
err := <-errCh
|
||||
|
||||
// append to errors
|
||||
if err != nil {
|
||||
gerrors = append(gerrors, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// return the errors
|
||||
if len(gerrors) > 0 {
|
||||
return errors.New("errors: " + strings.Join(gerrors, "\n"))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Task) Status() string {
|
||||
t.mtx.RLock()
|
||||
defer t.mtx.RUnlock()
|
||||
return t.status
|
||||
}
|
||||
|
||||
// Broker sets the micro broker
|
||||
func WithBroker(b broker.Broker) task.Option {
|
||||
return func(o *task.Options) {
|
||||
if o.Context == nil {
|
||||
o.Context = context.Background()
|
||||
}
|
||||
o.Context = context.WithValue(o.Context, brokerKey{}, b)
|
||||
}
|
||||
}
|
||||
|
||||
// NewTask returns a new broker task
|
||||
func NewTask(opts ...task.Option) task.Task {
|
||||
options := task.Options{
|
||||
Context: context.Background(),
|
||||
}
|
||||
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
|
||||
if options.Pool == 0 {
|
||||
options.Pool = 1
|
||||
}
|
||||
|
||||
b, ok := options.Context.Value(brokerKey{}).(broker.Broker)
|
||||
if !ok {
|
||||
b = broker.DefaultBroker
|
||||
}
|
||||
|
||||
return &Task{
|
||||
Broker: b,
|
||||
Options: options,
|
||||
}
|
||||
}
|
59
sync/task/local/local.go
Normal file
59
sync/task/local/local.go
Normal file
@ -0,0 +1,59 @@
|
||||
// Package local provides a local task runner
|
||||
package local
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/micro/go-micro/sync/task"
|
||||
)
|
||||
|
||||
type localTask struct {
|
||||
opts task.Options
|
||||
mtx sync.RWMutex
|
||||
status string
|
||||
}
|
||||
|
||||
func (l *localTask) Run(t task.Command) error {
|
||||
ch := make(chan error, l.opts.Pool)
|
||||
|
||||
for i := 0; i < l.opts.Pool; i++ {
|
||||
go func() {
|
||||
ch <- t.Execute()
|
||||
}()
|
||||
}
|
||||
|
||||
var err error
|
||||
|
||||
for i := 0; i < l.opts.Pool; i++ {
|
||||
er := <-ch
|
||||
if err != nil {
|
||||
err = er
|
||||
l.mtx.Lock()
|
||||
l.status = fmt.Sprintf("command [%s] status: %s", t.Name, err.Error())
|
||||
l.mtx.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
close(ch)
|
||||
return err
|
||||
}
|
||||
|
||||
func (l *localTask) Status() string {
|
||||
l.mtx.RLock()
|
||||
defer l.mtx.RUnlock()
|
||||
return l.status
|
||||
}
|
||||
|
||||
func NewTask(opts ...task.Option) task.Task {
|
||||
var options task.Options
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
if options.Pool == 0 {
|
||||
options.Pool = 1
|
||||
}
|
||||
return &localTask{
|
||||
opts: options,
|
||||
}
|
||||
}
|
83
sync/task/task.go
Normal file
83
sync/task/task.go
Normal file
@ -0,0 +1,83 @@
|
||||
// Package task provides an interface for distributed jobs
|
||||
package task
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Task represents a distributed task
|
||||
type Task interface {
|
||||
// Run runs a command immediately until completion
|
||||
Run(Command) error
|
||||
// Status provides status of last execution
|
||||
Status() string
|
||||
}
|
||||
|
||||
// Command to be executed
|
||||
type Command struct {
|
||||
Name string
|
||||
Func func() error
|
||||
}
|
||||
|
||||
// Schedule represents a time or interval at which a task should run
|
||||
type Schedule struct {
|
||||
// When to start the schedule. Zero time means immediately
|
||||
Time time.Time
|
||||
// Non zero interval dictates an ongoing schedule
|
||||
Interval time.Duration
|
||||
}
|
||||
|
||||
type Options struct {
|
||||
// Pool size for workers
|
||||
Pool int
|
||||
// Alternative options
|
||||
Context context.Context
|
||||
}
|
||||
|
||||
type Option func(o *Options)
|
||||
|
||||
func (c Command) Execute() error {
|
||||
return c.Func()
|
||||
}
|
||||
|
||||
func (c Command) String() string {
|
||||
return c.Name
|
||||
}
|
||||
|
||||
func (s Schedule) Run() <-chan time.Time {
|
||||
d := s.Time.Sub(time.Now())
|
||||
|
||||
ch := make(chan time.Time, 1)
|
||||
|
||||
go func() {
|
||||
// wait for start time
|
||||
<-time.After(d)
|
||||
|
||||
// zero interval
|
||||
if s.Interval == time.Duration(0) {
|
||||
ch <- time.Now()
|
||||
close(ch)
|
||||
return
|
||||
}
|
||||
|
||||
// start ticker
|
||||
for t := range time.Tick(s.Interval) {
|
||||
ch <- t
|
||||
}
|
||||
}()
|
||||
|
||||
return ch
|
||||
}
|
||||
|
||||
func (s Schedule) String() string {
|
||||
return fmt.Sprintf("%d-%d", s.Time.Unix(), s.Interval)
|
||||
}
|
||||
|
||||
// WithPool sets the pool size for concurrent work
|
||||
func WithPool(i int) Option {
|
||||
return func(o *Options) {
|
||||
o.Pool = i
|
||||
}
|
||||
}
|
18
sync/time/local/local.go
Normal file
18
sync/time/local/local.go
Normal file
@ -0,0 +1,18 @@
|
||||
// Package local provides a local clock
|
||||
package local
|
||||
|
||||
import (
|
||||
gotime "time"
|
||||
|
||||
"github.com/micro/go-micro/sync/time"
|
||||
)
|
||||
|
||||
type Time struct{}
|
||||
|
||||
func (t *Time) Now() (gotime.Time, error) {
|
||||
return gotime.Now(), nil
|
||||
}
|
||||
|
||||
func NewTime(opts ...time.Option) time.Time {
|
||||
return new(Time)
|
||||
}
|
51
sync/time/ntp/ntp.go
Normal file
51
sync/time/ntp/ntp.go
Normal file
@ -0,0 +1,51 @@
|
||||
// Package ntp provides ntp synchronized time
|
||||
package ntp
|
||||
|
||||
import (
|
||||
"context"
|
||||
gotime "time"
|
||||
|
||||
"github.com/beevik/ntp"
|
||||
"github.com/micro/go-micro/sync/time"
|
||||
)
|
||||
|
||||
type ntpTime struct {
|
||||
server string
|
||||
}
|
||||
|
||||
type ntpServerKey struct{}
|
||||
|
||||
func (n *ntpTime) Now() (gotime.Time, error) {
|
||||
return ntp.Time(n.server)
|
||||
}
|
||||
|
||||
// NewTime returns ntp time
|
||||
func NewTime(opts ...time.Option) time.Time {
|
||||
options := time.Options{
|
||||
Context: context.Background(),
|
||||
}
|
||||
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
|
||||
server := "time.google.com"
|
||||
|
||||
if k, ok := options.Context.Value(ntpServerKey{}).(string); ok {
|
||||
server = k
|
||||
}
|
||||
|
||||
return &ntpTime{
|
||||
server: server,
|
||||
}
|
||||
}
|
||||
|
||||
// WithServer sets the ntp server
|
||||
func WithServer(s string) time.Option {
|
||||
return func(o *time.Options) {
|
||||
if o.Context == nil {
|
||||
o.Context = context.Background()
|
||||
}
|
||||
o.Context = context.WithValue(o.Context, ntpServerKey{}, s)
|
||||
}
|
||||
}
|
18
sync/time/time.go
Normal file
18
sync/time/time.go
Normal file
@ -0,0 +1,18 @@
|
||||
// Package time provides clock synchronization
|
||||
package time
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Time returns synchronized time
|
||||
type Time interface {
|
||||
Now() (time.Time, error)
|
||||
}
|
||||
|
||||
type Options struct {
|
||||
Context context.Context
|
||||
}
|
||||
|
||||
type Option func(o *Options)
|
Loading…
Reference in New Issue
Block a user