Update split SDK to 6.0.2 to fix sync bug (#17060)

* Update split SDK to 6.0.2 to fix sync bug

* Vendor and tidy
Этот коммит содержится в:
Joram Wilander
2021-03-04 11:16:08 -05:00
коммит произвёл GitHub
родитель fa2ecad0a9
Коммит aba00a3cfd
150 изменённых файлов: 2500 добавлений и 1960 удалений

216
vendor/github.com/splitio/go-toolkit/v3/sse/sse.go сгенерированный поставляемый
Просмотреть файл

@@ -1,216 +0,0 @@
package sse
import (
"bufio"
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"sync"
"sync/atomic"
"time"
"github.com/splitio/go-toolkit/v3/logging"
)
const (
// OK It could connect streaming
OK = iota
// ErrorOnClientCreation Could not create client
ErrorOnClientCreation
// ErrorRequestPerformed Could not perform request
ErrorRequestPerformed
// ErrorConnectToStreaming Could not connect to streaming
ErrorConnectToStreaming
// ErrorReadingStream Error in streaming
ErrorReadingStream
// ErrorKeepAlive timedout
ErrorKeepAlive
// ErrorInternal Internal error for streaming
ErrorInternal
// ErrorUnexpected unexpected error occures
ErrorUnexpected
)
var sseDelimiter [2]byte = [...]byte{':', ' '}
var sseData [4]byte = [...]byte{'d', 'a', 't', 'a'}
var sseKeepAlive [10]byte = [...]byte{':', 'k', 'e', 'e', 'p', 'a', 'l', 'i', 'v', 'e'}
// SSEClient struct
type SSEClient struct {
url string
client http.Client
status chan int
shutdown chan struct{}
timeout int
logger logging.LoggerInterface
}
// NewSSEClient creates new SSEClient
func NewSSEClient(url string, status chan int, timeout int, logger logging.LoggerInterface) (*SSEClient, error) {
if cap(status) < 1 {
return nil, errors.New("Status channel should have length")
}
if timeout < 1 {
return nil, errors.New("Timeout should be higher than 0")
}
return &SSEClient{
url: url,
client: http.Client{},
status: status,
shutdown: make(chan struct{}, 1),
timeout: timeout,
logger: logger,
}, nil
}
// Shutdown stops SSE
func (l *SSEClient) Shutdown() {
select {
case l.shutdown <- struct{}{}:
default:
l.logger.Error("Shutdown already in progress")
}
}
func parseData(raw []byte) (map[string]interface{}, error) {
data := make(map[string]interface{})
err := json.Unmarshal(raw, &data)
if err != nil {
return nil, fmt.Errorf("error parsing json: %w", err)
}
return data, nil
}
func (l *SSEClient) readEvent(reader *bufio.Reader) (map[string]interface{}, error) {
line, err := reader.ReadBytes('\n')
if err != nil {
return nil, err
}
if len(line) < 2 {
return nil, nil
}
splitted := bytes.Split(line, sseDelimiter[:])
if bytes.Compare(splitted[0], sseData[:]) != 0 {
return nil, nil
}
raw := bytes.TrimSpace(splitted[1])
l.logger.Debug("LINE:", string(line))
data, err := parseData(raw)
if err != nil {
l.logger.Error("Error parsing event: ", err)
return nil, nil
}
return data, nil
}
func parseHTTPError(resp *http.Response) int {
if resp.StatusCode >= http.StatusInternalServerError {
return ErrorInternal
}
return ErrorConnectToStreaming
}
// Do starts streaming
func (l *SSEClient) Do(params map[string]string, callback func(e map[string]interface{})) {
select {
case <-l.shutdown:
// Skipping previous shutdown
default:
}
ctx, cancel := context.WithCancel(context.Background())
shouldRun := atomic.Value{}
shouldRun.Store(false)
activeGoroutines := sync.WaitGroup{}
defer func() {
l.logger.Info("SSE streaming exiting")
shouldRun.Store(false)
cancel()
activeGoroutines.Wait()
}()
req, err := http.NewRequest("GET", l.url, nil)
if err != nil {
l.logger.Error(err)
l.status <- ErrorOnClientCreation
return
}
req = req.WithContext(ctx)
query := req.URL.Query()
for key, value := range params {
query.Add(key, value)
}
req.URL.RawQuery = query.Encode()
req.Header.Set("Accept", "text/event-stream")
resp, err := l.client.Do(req)
if err != nil {
l.logger.Error(err)
l.status <- ErrorRequestPerformed
return
}
if resp.StatusCode != 200 {
l.status <- parseHTTPError(resp)
return
}
defer resp.Body.Close()
l.status <- OK
reader := bufio.NewReader(resp.Body)
eventChannel := make(chan map[string]interface{}, 1000)
shouldRun.Store(true)
go func() {
for shouldRun.Load().(bool) {
event, err := l.readEvent(reader)
if err != nil {
if shouldRun.Load().(bool) {
l.logger.Error(err)
}
close(eventChannel)
return
}
eventChannel <- event
}
}()
// Create timeout timer in case SSE dont receive notifications or keepalive messages
idleDuration := time.Duration(l.timeout) * time.Second
keepAliveTimer := time.NewTimer(idleDuration)
defer keepAliveTimer.Stop()
for {
// Resetting timer
keepAliveTimer.Reset(idleDuration)
select {
case <-l.shutdown:
l.logger.Info("Shutting down listener")
return
case event, ok := <-eventChannel:
if !ok {
l.status <- ErrorReadingStream
return
}
if event != nil {
activeGoroutines.Add(1)
go func() {
defer activeGoroutines.Done()
callback(event)
}()
}
case <-keepAliveTimer.C: // Timedout
l.status <- ErrorKeepAlive
return
}
}
}

Просмотреть файл

Просмотреть файл

@@ -2,81 +2,66 @@ package asynctask
import (
"fmt"
"sync/atomic"
"time"
"github.com/splitio/go-toolkit/v3/logging"
"github.com/splitio/go-toolkit/v4/logging"
"github.com/splitio/go-toolkit/v4/struct/traits/lifecycle"
)
// AsyncTask is a struct that wraps tasks that should run periodically and can be remotely stopped & started,
// as well as making it's status (running/stopped) available.
type AsyncTask struct {
task func(l logging.LoggerInterface) error
name string
running atomic.Value
incoming chan int
period int
onInit func(l logging.LoggerInterface) error
onStop func(l logging.LoggerInterface)
logger logging.LoggerInterface
finished atomic.Value
finishChan chan struct{}
lifecycle lifecycle.Manager
task func(l logging.LoggerInterface) error
name string
incoming chan int
period int
onInit func(l logging.LoggerInterface) error
onStop func(l logging.LoggerInterface)
logger logging.LoggerInterface
}
const (
taskMessageStop = iota
taskMessageWakeup
taskMessageWakeup = iota
)
func (t *AsyncTask) _running() bool {
res, ok := t.running.Load().(bool)
if !ok {
t.logger.Error("Error parsing async task status flag")
return false
}
return res
}
// Start initiates the task. It wraps the execution in a closure guarded by a call to recover() in order
// to prevent the main application from crashin if something goes wrong while the sdk interacts with the backend.
func (t *AsyncTask) Start() {
if t._running() {
if !t.lifecycle.BeginInitialization() {
if t.logger != nil {
t.logger.Warning("Task %s is already running. Aborting new execution.", t.name)
t.logger.Warning(fmt.Sprintf("Task %s is not idle. Aborting new execution.", t.name))
}
return
}
t.running.Store(true)
go func() {
defer func() {
t.finished.Store(true)
t.finishChan <- struct{}{}
}()
defer func() {
if r := recover(); r != nil {
t.running.Store(false)
if t.logger != nil {
t.logger.Error(fmt.Sprintf(
"AsyncTask %s is panicking! Delaying execution for %d seconds (1 period)",
"AsyncTask %s is panicking! shutting down. Consider restarting this instance and raising an issue",
t.name,
t.period,
))
t.logger.Error(r)
}
time.Sleep(time.Duration(t.period) * time.Second)
}
}()
defer t.lifecycle.ShutdownComplete()
if !t.lifecycle.InitializationComplete() {
return
}
// If there's an initialization function, execute it
if t.onInit != nil {
err := t.onInit(t.logger)
if err != nil {
// If something goes wrong during initialization, abort.
if err != nil { // If something goes wrong during initialization, abort.
if t.logger != nil {
t.logger.Error(err.Error())
}
t.lifecycle.AbnormalShutdown()
return
}
}
@@ -86,8 +71,19 @@ func (t *AsyncTask) Start() {
taskTimer := time.NewTimer(idleDuration)
defer taskTimer.Stop()
if t.onStop != nil {
defer t.onStop(t.logger)
}
// Task execution
for t._running() {
for {
select {
case <-t.lifecycle.ShutdownRequested():
return
case <-t.incoming: // wake up signal
case <-taskTimer.C: // Timedout
}
// Run the wrapped task and handle the returned error if any.
err := t.task(t.logger)
if err != nil && t.logger != nil {
@@ -96,22 +92,6 @@ func (t *AsyncTask) Start() {
// Resetting timer
taskTimer.Reset(idleDuration)
// Wait for either a timeout or an interruption (can be a stop signal or a wake up)
select {
case msg := <-t.incoming:
switch msg {
case taskMessageStop:
t.running.Store(false)
case taskMessageWakeup:
}
case <-taskTimer.C: // Timedout
}
}
// Post-execution cleanup
if t.onStop != nil {
t.onStop(t.logger)
}
}()
}
@@ -127,19 +107,12 @@ func (t *AsyncTask) sendSignal(signal int) error {
// Stop executes onStop hook if any, blocks until its done (if blocking = true) and prevents future executions of the task.
func (t *AsyncTask) Stop(blocking bool) error {
if !t._running() || t.finished.Load().(bool) {
// Task already stopped
return nil
}
if err := t.sendSignal(taskMessageStop); err != nil {
// If the signal couldnt be sent, return error!
return err
if !t.lifecycle.BeginShutdown() {
return fmt.Errorf("task '%s' not running", t.name)
}
if blocking {
// If blocking was set to true, wait until an empty strcut is pushed into the channel
<-t.finishChan
t.lifecycle.AwaitShutdownComplete()
}
return nil
}
@@ -151,7 +124,7 @@ func (t *AsyncTask) WakeUp() error {
// IsRunning returns true if the task is currently running
func (t *AsyncTask) IsRunning() bool {
return t._running()
return t.lifecycle.IsRunning()
}
// NewAsyncTask creates a new task and returns a pointer to it
@@ -164,16 +137,14 @@ func NewAsyncTask(
logger logging.LoggerInterface,
) *AsyncTask {
t := AsyncTask{
name: name,
task: task,
period: period,
onInit: onInit,
onStop: onStop,
logger: logger,
incoming: make(chan int, 10),
finishChan: make(chan struct{}, 1),
name: name,
task: task,
period: period,
onInit: onInit,
onStop: onStop,
logger: logger,
incoming: make(chan int, 10),
}
t.running.Store(false)
t.finished.Store(false)
t.lifecycle.Setup()
return &t
}

35
vendor/github.com/splitio/go-toolkit/v4/backoff/backoff.go сгенерированный поставляемый Обычный файл
Просмотреть файл

@@ -0,0 +1,35 @@
package backoff
import (
"math"
"sync/atomic"
"time"
)
// Interface is the backoff interface
type Interface interface {
Next() time.Duration
Reset()
}
// Impl implements the Backoff interface
type Impl struct {
base int64
current int64
}
// Next returns how long to wait and updates the current count
func (b *Impl) Next() time.Duration {
current := atomic.AddInt64(&b.current, 1)
return time.Duration(math.Pow(float64(b.base), float64(current))) * time.Second
}
// Reset sets the current count to 0
func (b *Impl) Reset() {
atomic.StoreInt64(&b.current, 0)
}
// New creates a new Backoffer
func New() *Impl {
return &Impl{base: 2}
}

Просмотреть файл

Просмотреть файл

Просмотреть файл

Просмотреть файл

Просмотреть файл

Просмотреть файл

Просмотреть файл

Просмотреть файл

Просмотреть файл

Просмотреть файл

Просмотреть файл

Просмотреть файл

Просмотреть файл

Просмотреть файл

Просмотреть файл

Просмотреть файл

Просмотреть файл

Просмотреть файл

Просмотреть файл

@@ -3,7 +3,7 @@ package helpers
import (
"fmt"
"github.com/splitio/go-toolkit/v3/redis"
"github.com/splitio/go-toolkit/v4/redis"
)
const (

Просмотреть файл

Просмотреть файл

Просмотреть файл

31
vendor/github.com/splitio/go-toolkit/v4/sse/errors.go сгенерированный поставляемый Обычный файл
Просмотреть файл

@@ -0,0 +1,31 @@
package sse
import (
"errors"
)
// ErrNotIdle is the error tor eturn when Do() gets called on an already running client.
var ErrNotIdle = errors.New("sse client already running")
// ErrReadingStream is the error to return when channel event channel is closed because of an error reading the stream
var ErrReadingStream = errors.New("sse channel closed")
// ErrTimeout is the error to return when keepalive timeout is exceeded
var ErrTimeout = errors.New("timeout exceeeded")
// ErrConnectionFailed contains a nested error
type ErrConnectionFailed struct {
wrapped error
}
// Error returns the error as a string
func (e *ErrConnectionFailed) Error() string {
return "error connecting: " + e.wrapped.Error()
}
// Unwrap returns the wrapped error
func (e *ErrConnectionFailed) Unwrap() error {
return e.wrapped
}
var _ error = &ErrConnectionFailed{}

120
vendor/github.com/splitio/go-toolkit/v4/sse/event.go сгенерированный поставляемый Обычный файл
Просмотреть файл

@@ -0,0 +1,120 @@
package sse
import (
"strconv"
"strings"
"sync"
)
const (
sseDelimiter = ":"
sseData = "data"
sseEvent = "event"
sseID = "id"
sseRetry = "retry"
)
// RawEvent interface contains the methods that expose the incoming SSE properties
type RawEvent interface {
ID() string
Event() string
Data() string
Retry() int64
IsError() bool
IsEmpty() bool
}
// RawEventImpl represents an incoming SSE event
type RawEventImpl struct {
id string
event string
data string
retry int64
}
// ID returns the event id
func (r *RawEventImpl) ID() string { return r.id }
// Event returns the event type
func (r *RawEventImpl) Event() string { return r.event }
// Data returns the event associated data
func (r *RawEventImpl) Data() string { return r.data }
// Retry returns the expected retry time
func (r *RawEventImpl) Retry() int64 { return r.retry }
// IsError returns true if the message is an error
func (r *RawEventImpl) IsError() bool { return r.event == "error" }
// IsEmpty returns true if the event contains no id, event type and data
func (r *RawEventImpl) IsEmpty() bool { return r.event == "" && r.id == "" && r.data == "" }
// EventBuilder interface
type EventBuilder interface {
AddLine(string)
Build() *RawEventImpl
}
// EventBuilderImpl implenets the EventBuilder interface. Used to parse incoming event lines
type EventBuilderImpl struct {
includesComment bool
mutex sync.Mutex
lines []string
}
// AddLine adds a new line belonging to the currently being processed event
func (b *EventBuilderImpl) AddLine(line string) {
if strings.HasPrefix(line, sseDelimiter) {
// Ignore comments
return
}
b.mutex.Lock()
defer b.mutex.Unlock()
b.lines = append(b.lines, line)
}
// Build processes all the added lines and builds the event
func (b *EventBuilderImpl) Build() *RawEventImpl {
b.mutex.Lock()
defer b.mutex.Unlock()
if len(b.lines) == 0 { // Empty event
return &RawEventImpl{}
}
e := &RawEventImpl{}
for _, line := range b.lines {
splitted := strings.SplitN(line, sseDelimiter, 2)
if len(splitted) != 2 {
// TODO: log invalid line.
continue
}
switch splitted[0] {
case sseID:
e.id = strings.TrimSpace(splitted[1])
case sseData:
e.data = strings.TrimSpace(splitted[1])
case sseEvent:
e.event = strings.TrimSpace(splitted[1])
case sseRetry:
e.retry, _ = strconv.ParseInt(strings.TrimSpace(splitted[1]), 10, 64)
}
}
return e
}
// Reset clears the lines accepted
func (b *EventBuilderImpl) Reset() {
b.mutex.Lock()
defer b.mutex.Unlock()
b.lines = []string{}
}
// NewEventBuilder constructs a new event builder
func NewEventBuilder() *EventBuilderImpl {
return &EventBuilderImpl{lines: []string{}}
}

174
vendor/github.com/splitio/go-toolkit/v4/sse/sse.go сгенерированный поставляемый Обычный файл
Просмотреть файл

@@ -0,0 +1,174 @@
package sse
import (
"bufio"
"context"
"errors"
"fmt"
"net/http"
"sync"
"time"
"github.com/splitio/go-toolkit/v4/logging"
"github.com/splitio/go-toolkit/v4/struct/traits/lifecycle"
)
const (
statusIdle = iota
statusRunning
statusShuttingDown
endOfLineChar = '\n'
endOfLineStr = "\n"
)
// Client struct
type Client struct {
lifecycle lifecycle.Manager
url string
client http.Client
timeout time.Duration
logger logging.LoggerInterface
}
// NewClient creates new SSEClient
func NewClient(url string, timeout int, logger logging.LoggerInterface) (*Client, error) {
if timeout < 1 {
return nil, errors.New("Timeout should be higher than 0")
}
client := &Client{
url: url,
client: http.Client{},
timeout: time.Duration(timeout) * time.Second,
logger: logger,
}
client.lifecycle.Setup()
return client, nil
}
func (l *Client) readEvents(in *bufio.Reader, out chan<- RawEvent) {
eventBuilder := NewEventBuilder()
for {
line, err := in.ReadString(endOfLineChar)
l.logger.Debug("Incoming SSE line: ", line)
if err != nil {
if l.lifecycle.IsRunning() { // If it's supposed to be running, log an error
l.logger.Error(err)
}
close(out)
return
}
if line != endOfLineStr {
eventBuilder.AddLine(line)
continue
}
l.logger.Debug("Building SSE event")
if event := eventBuilder.Build(); event != nil {
out <- event
}
eventBuilder.Reset()
}
}
// Do starts streaming
func (l *Client) Do(params map[string]string, callback func(e RawEvent)) error {
if !l.lifecycle.BeginInitialization() {
return ErrNotIdle
}
activeGoroutines := sync.WaitGroup{}
ctx, cancel := context.WithCancel(context.Background())
defer func() {
l.logger.Info("SSE streaming exiting")
cancel()
activeGoroutines.Wait()
l.lifecycle.ShutdownComplete()
}()
req, err := l.buildCancellableRequest(ctx, params)
if err != nil {
return &ErrConnectionFailed{wrapped: fmt.Errorf("error building request: %w", err)}
}
resp, err := l.client.Do(req)
if err != nil {
return &ErrConnectionFailed{wrapped: fmt.Errorf("error issuing request: %w", err)}
}
if resp.StatusCode != 200 {
return &ErrConnectionFailed{wrapped: fmt.Errorf("sse request status code: %d", resp.StatusCode)}
}
defer resp.Body.Close()
if !l.lifecycle.InitializationComplete() {
return nil
}
reader := bufio.NewReader(resp.Body)
eventChannel := make(chan RawEvent, 1000)
go l.readEvents(reader, eventChannel)
// Create timeout timer in case SSE dont receive notifications or keepalive messages
keepAliveTimer := time.NewTimer(l.timeout)
defer keepAliveTimer.Stop()
for {
select {
case <-l.lifecycle.ShutdownRequested():
l.logger.Info("Shutting down listener")
return nil
case event, ok := <-eventChannel:
keepAliveTimer.Reset(l.timeout)
if !ok {
if l.lifecycle.IsRunning() {
return ErrReadingStream
}
return nil
}
if event.IsEmpty() {
continue // don't forward empty/comment events
}
activeGoroutines.Add(1)
go func() {
defer activeGoroutines.Done()
callback(event)
}()
case <-keepAliveTimer.C: // Timeout
l.logger.Warning("SSE idle timeout.")
l.lifecycle.AbnormalShutdown()
return ErrTimeout
}
}
}
// Shutdown stops SSE
func (l *Client) Shutdown(blocking bool) {
if !l.lifecycle.BeginShutdown() {
l.logger.Info("SSE client stopped or shutdown in progress. Ignoring.")
return
}
if blocking {
l.lifecycle.AwaitShutdownComplete()
}
}
func (l *Client) buildCancellableRequest(ctx context.Context, params map[string]string) (*http.Request, error) {
req, err := http.NewRequest("GET", l.url, nil)
if err != nil {
return nil, fmt.Errorf("error instantiating request: %w", err)
}
req = req.WithContext(ctx)
query := req.URL.Query()
for key, value := range params {
query.Add(key, value)
}
req.URL.RawQuery = query.Encode()
req.Header.Set("Accept", "text/event-stream")
return req, nil
}

108
vendor/github.com/splitio/go-toolkit/v4/struct/traits/lifecycle/lifecycle.go сгенерированный поставляемый Обычный файл
Просмотреть файл

@@ -0,0 +1,108 @@
package lifecycle
import (
"sync"
"sync/atomic"
)
// Status constants
const (
StatusIdle = iota
StatusStarting
StatusInitializationCancelled
StatusRunning
StatusStopping
)
// Status type alias
type Status = int32
// Manager is a trait to be embedded in structs that manage the lifecycle of goroutines.
// The trait enables the struct to easily switch between states and await proper shutdown
type Manager struct {
status int32
c *sync.Cond
shutdown chan struct{}
}
// Setup must be called in the struct constructor
func (l *Manager) Setup() {
l.c = sync.NewCond(&sync.Mutex{})
l.shutdown = make(chan struct{}, 1)
}
// BeginInitialization should be called in the .Start() method (or whichever begins the async work)
func (l *Manager) BeginInitialization() bool {
return atomic.CompareAndSwapInt32(&l.status, StatusIdle, StatusStarting)
}
// InitializationComplete should be called just prior to the `go ...` directive starting the async work
func (l *Manager) InitializationComplete() bool {
if !atomic.CompareAndSwapInt32(&l.status, StatusStarting, StatusRunning) {
atomic.StoreInt32(&l.status, StatusStopping)
return false
}
return true
}
// BeginShutdown should be called on the .Stop() method or whichever makes a request for the async work to stop
func (l *Manager) BeginShutdown() bool {
// If we're currently initializing but not yet running, just change the status.
if atomic.CompareAndSwapInt32(&l.status, StatusStarting, StatusInitializationCancelled) {
return true
}
if !atomic.CompareAndSwapInt32(&l.status, StatusRunning, StatusStopping) {
return false
}
l.shutdown <- struct{}{}
return true
}
// ShutdownComplete should be called just before the goroutine exits. (ie: it should be the FIRST deferred func)
func (l *Manager) ShutdownComplete() {
// clean up status channel in case a Stop occurred while the task was exiting on its own
select {
case <-l.shutdown:
default:
}
l.c.L.Lock()
atomic.StoreInt32(&l.status, StatusIdle)
l.c.Broadcast()
l.c.L.Unlock()
}
// AwaitShutdownComplete can be called in case you need to join against the goroutine's end
func (l *Manager) AwaitShutdownComplete() {
for {
l.c.L.Lock()
if atomic.LoadInt32(&l.status) == StatusIdle {
l.c.L.Unlock()
return
}
l.c.Wait()
l.c.L.Unlock()
}
}
// ShutdownRequested should be queried in a select statement, which should react by terminating the goroutine
func (l *Manager) ShutdownRequested() <-chan struct{} {
return l.shutdown
}
// AbnormalShutdown should be called when the goroutine exits without Stop being called.
func (l *Manager) AbnormalShutdown() {
atomic.CompareAndSwapInt32(&l.status, StatusRunning, StatusStopping)
}
// Status Returns the current status as an int32 constant
func (l *Manager) Status() int32 {
return atomic.LoadInt32(&l.status)
}
// IsRunning returns true if the BG work is still going on
func (l *Manager) IsRunning() bool {
return atomic.LoadInt32(&l.status) == StatusRunning
}

41
vendor/github.com/splitio/go-toolkit/v4/sync/atomicbool.go сгенерированный поставляемый Обычный файл
Просмотреть файл

@@ -0,0 +1,41 @@
package sync
import (
"sync/atomic"
)
const (
falseValue = 0
trueValue = 1
)
type AtomicBool struct {
value uint32
}
func (b *AtomicBool) Set() {
atomic.StoreUint32(&b.value, trueValue)
}
func (b *AtomicBool) Unset() {
atomic.StoreUint32(&b.value, falseValue)
}
func (b *AtomicBool) IsSet() bool {
return atomic.LoadUint32(&b.value) == trueValue
}
func (b *AtomicBool) TestAndSet() bool {
return atomic.CompareAndSwapUint32(&b.value, falseValue, trueValue)
}
func (b *AtomicBool) TestAndClear() bool {
return atomic.CompareAndSwapUint32(&b.value, trueValue, falseValue)
}
func NewAtomicBool(initialValue bool) *AtomicBool {
if initialValue {
return &AtomicBool{value: trueValue}
}
return &AtomicBool{}
}

Просмотреть файл

@@ -2,9 +2,11 @@ package workerpool
import (
"fmt"
"github.com/splitio/go-toolkit/v3/logging"
"sync"
"time"
"github.com/splitio/go-toolkit/v4/logging"
"github.com/splitio/go-toolkit/v4/struct/traits/lifecycle"
)
const (
@@ -13,11 +15,11 @@ const (
// WorkerAdmin struct handles multiple worker execution, popping jobs from a single queue
type WorkerAdmin struct {
queue chan interface{}
signalsMutex sync.RWMutex
signals map[string]chan int
logger logging.LoggerInterface
wg sync.WaitGroup
queue chan interface{}
mutex sync.RWMutex
//signals map[string]chan int
workers map[string]*workerWrapper
logger logging.LoggerInterface
}
// Worker interface should be implemented by concrete workers that will perform the actual job
@@ -34,53 +36,77 @@ type Worker interface {
FailureTime() int64
}
func (a *WorkerAdmin) workerWrapper(w Worker) {
a.signalsMutex.Lock()
a.signals[w.Name()] = make(chan int, 10)
a.signalsMutex.Unlock()
defer a.wg.Done()
type workerWrapper struct {
w Worker
lifecycle lifecycle.Manager
queue <-chan interface{}
logger logging.LoggerInterface
}
func (w *workerWrapper) Start() {
if !w.lifecycle.BeginInitialization() {
w.logger.Error(fmt.Sprintf("initialization of worker '%s' aborted. Worker not idle.", w.w.Name()))
return
}
go w.do()
}
func (w *workerWrapper) Stop(blocking bool) {
if !w.lifecycle.BeginShutdown() {
w.logger.Error(fmt.Sprintf("shutodwn of worker '%s' aborted. Worker not running.", w.w.Name()))
return
}
if blocking {
w.lifecycle.AwaitShutdownComplete()
}
}
func (w *workerWrapper) do() {
defer func() {
if r := recover(); r != nil {
a.logger.Error(fmt.Sprintf(
w.logger.Error(fmt.Sprintf(
"Worker %s is panicking with the following error \"%s\" and will be shutted down.",
w.Name(),
w.w.Name(),
r,
))
}
if a.signals != nil { // This should ALWAYS be the case, but just in case... we don't want to panic here.
a.signalsMutex.Lock()
delete(a.signals, w.Name())
a.signalsMutex.Unlock()
w.lifecycle.AbnormalShutdown()
}
}()
defer w.Cleanup()
defer w.lifecycle.ShutdownComplete()
defer w.w.Cleanup()
if !w.lifecycle.InitializationComplete() {
return
}
for {
a.signalsMutex.RLock()
signal := a.signals[w.Name()]
a.signalsMutex.RUnlock()
select {
case msg := <-signal:
switch msg {
case workerSignalStop:
return
}
case msg := <-a.queue:
if err := w.DoWork(msg); err != nil {
w.OnError(err)
time.Sleep(time.Duration(w.FailureTime()) * time.Millisecond)
case <-w.lifecycle.ShutdownRequested():
return
case msg := <-w.queue:
if err := w.w.DoWork(msg); err != nil {
w.w.OnError(err)
time.Sleep(time.Duration(w.w.FailureTime()) * time.Millisecond)
}
}
}
}
func newWorkerWraper(w Worker, logger logging.LoggerInterface, queue <-chan interface{}) *workerWrapper {
worker := &workerWrapper{w: w, queue: queue, logger: logger}
worker.lifecycle.Setup()
worker.Start()
return worker
}
// AddWorker registers a new worker in the admin
func (a *WorkerAdmin) AddWorker(w Worker) {
if w == nil {
a.logger.Error("AddWorker called with nil")
return
}
a.wg.Add(1)
go a.workerWrapper(w)
a.mutex.Lock()
defer a.mutex.Unlock()
a.workers[w.Name()] = newWorkerWraper(w, a.logger, a.queue)
}
// QueueMessage adds a new message that will be popped by a worker and processed
@@ -98,48 +124,37 @@ func (a *WorkerAdmin) QueueMessage(m interface{}) bool {
}
// StopWorker ends the worker's event loop, preventing it from picking further jobs
func (a *WorkerAdmin) StopWorker(name string) error {
a.signalsMutex.RLock()
c, ok := a.signals[name]
a.signalsMutex.RUnlock()
func (a *WorkerAdmin) StopWorker(name string, blocking bool) error {
a.mutex.Lock()
defer a.mutex.Unlock()
w, ok := a.workers[name]
if !ok {
return fmt.Errorf("Worker %s doesn't exist, hence it cannot be stopped", name)
}
select {
case c <- workerSignalStop:
default:
return fmt.Errorf("Couldn't send stop signal to worker %s", name)
}
w.Stop(blocking)
return nil
}
// StopAll ends all worker's event loops
func (a *WorkerAdmin) StopAll(blocking bool) error {
failed := make([]string, 0)
workerNames := make([]string, 0)
// Get worker names safely
a.signalsMutex.RLock()
for workerName := range a.signals {
workerNames = append(workerNames, workerName)
}
a.signalsMutex.RUnlock()
for _, workerName := range workerNames {
err := a.StopWorker(workerName)
if err != nil {
a.logger.Error(err)
failed = append(failed, workerName)
wg := sync.WaitGroup{}
a.mutex.Lock()
defer a.mutex.Unlock()
for _, w := range a.workers {
if w != nil {
wg.Add(1)
go func(current *workerWrapper) {
current.Stop(true)
wg.Done()
}(w)
}
}
if len(failed) > 0 {
return fmt.Errorf("Workers %v failed to shutdown", failed)
}
if blocking {
a.wg.Wait()
wg.Wait()
}
return nil
}
@@ -150,16 +165,16 @@ func (a *WorkerAdmin) QueueSize() int {
// IsWorkerRunning returns true if the worker exists and is currently running
func (a *WorkerAdmin) IsWorkerRunning(name string) bool {
a.signalsMutex.RLock()
_, ok := a.signals[name]
a.signalsMutex.RUnlock()
return ok // We consider a worker to be running if it exists in the list of valid signal channels
a.mutex.RLock()
defer a.mutex.RUnlock()
x, ok := a.workers[name]
return ok && x.lifecycle.IsRunning()
}
// NewWorkerAdmin instantiates a new WorkerAdmin and returns a pointer to it.
func NewWorkerAdmin(queueSize int, logger logging.LoggerInterface) *WorkerAdmin {
return &WorkerAdmin{
signals: make(map[string]chan int, 0),
workers: make(map[string]*workerWrapper, 0),
logger: logger,
queue: make(chan interface{}, queueSize),
}