add queue package

master v1.0.1
kevin 4 years ago
parent 5f084fb7d2
commit 6fdee77fa9

@ -0,0 +1,44 @@
package queue
import (
"errors"
"sync/atomic"
"github.com/tal-tech/go-zero/core/logx"
)
var ErrNoAvailablePusher = errors.New("no available pusher")
type BalancedQueuePusher struct {
name string
pushers []Pusher
index uint64
}
func NewBalancedQueuePusher(pushers []Pusher) Pusher {
return &BalancedQueuePusher{
name: generateName(pushers),
pushers: pushers,
}
}
func (pusher *BalancedQueuePusher) Name() string {
return pusher.name
}
func (pusher *BalancedQueuePusher) Push(message string) error {
size := len(pusher.pushers)
for i := 0; i < size; i++ {
index := atomic.AddUint64(&pusher.index, 1) % uint64(size)
target := pusher.pushers[index]
if err := target.Push(message); err != nil {
logx.Error(err)
} else {
return nil
}
}
return ErrNoAvailablePusher
}

@ -0,0 +1,43 @@
package queue
import (
"fmt"
"strconv"
"testing"
"github.com/stretchr/testify/assert"
)
func TestBalancedQueuePusher(t *testing.T) {
const numPushers = 100
var pushers []Pusher
var mockedPushers []*mockedPusher
for i := 0; i < numPushers; i++ {
p := &mockedPusher{
name: "pusher:" + strconv.Itoa(i),
}
pushers = append(pushers, p)
mockedPushers = append(mockedPushers, p)
}
pusher := NewBalancedQueuePusher(pushers)
assert.True(t, len(pusher.Name()) > 0)
for i := 0; i < numPushers*1000; i++ {
assert.Nil(t, pusher.Push("item"))
}
var counts []int
for _, p := range mockedPushers {
counts = append(counts, p.count)
}
mean := calcMean(counts)
variance := calcVariance(mean, counts)
assert.True(t, variance < 100, fmt.Sprintf("too big variance - %.2f", variance))
}
func TestBalancedQueuePusher_NoAvailable(t *testing.T) {
pusher := NewBalancedQueuePusher(nil)
assert.True(t, len(pusher.Name()) == 0)
assert.Equal(t, ErrNoAvailablePusher, pusher.Push("item"))
}

@ -0,0 +1,10 @@
package queue
type (
Consumer interface {
Consume(string) error
OnEvent(event interface{})
}
ConsumerFactory func() (Consumer, error)
)

@ -0,0 +1,6 @@
package queue
type MessageQueue interface {
Start()
Stop()
}

@ -0,0 +1,31 @@
package queue
import "github.com/tal-tech/go-zero/core/errorx"
type MultiQueuePusher struct {
name string
pushers []Pusher
}
func NewMultiQueuePusher(pushers []Pusher) Pusher {
return &MultiQueuePusher{
name: generateName(pushers),
pushers: pushers,
}
}
func (pusher *MultiQueuePusher) Name() string {
return pusher.name
}
func (pusher *MultiQueuePusher) Push(message string) error {
var batchError errorx.BatchError
for _, each := range pusher.pushers {
if err := each.Push(message); err != nil {
batchError.Add(err)
}
}
return batchError.Err()
}

@ -0,0 +1,39 @@
package queue
import (
"fmt"
"math"
"strconv"
"testing"
"github.com/stretchr/testify/assert"
)
func TestMultiQueuePusher(t *testing.T) {
const numPushers = 100
var pushers []Pusher
var mockedPushers []*mockedPusher
for i := 0; i < numPushers; i++ {
p := &mockedPusher{
name: "pusher:" + strconv.Itoa(i),
}
pushers = append(pushers, p)
mockedPushers = append(mockedPushers, p)
}
pusher := NewMultiQueuePusher(pushers)
assert.True(t, len(pusher.Name()) > 0)
for i := 0; i < 1000; i++ {
_ = pusher.Push("item")
}
var counts []int
for _, p := range mockedPushers {
counts = append(counts, p.count)
}
mean := calcMean(counts)
variance := calcVariance(mean, counts)
assert.True(t, math.Abs(mean-1000*(1-failProba)) < 10)
assert.True(t, variance < 100, fmt.Sprintf("too big variance - %.2f", variance))
}

@ -0,0 +1,15 @@
package queue
type (
Producer interface {
AddListener(listener ProduceListener)
Produce() (string, bool)
}
ProduceListener interface {
OnProducerPause()
OnProducerResume()
}
ProducerFactory func() (Producer, error)
)

@ -0,0 +1,239 @@
package queue
import (
"runtime"
"sync"
"sync/atomic"
"time"
"github.com/tal-tech/go-zero/core/logx"
"github.com/tal-tech/go-zero/core/rescue"
"github.com/tal-tech/go-zero/core/stat"
"github.com/tal-tech/go-zero/core/threading"
"github.com/tal-tech/go-zero/core/timex"
)
const queueName = "queue"
type (
Queue struct {
name string
metrics *stat.Metrics
producerFactory ProducerFactory
producerRoutineGroup *threading.RoutineGroup
consumerFactory ConsumerFactory
consumerRoutineGroup *threading.RoutineGroup
producerCount int
consumerCount int
active int32
channel chan string
quit chan struct{}
listeners []Listener
eventLock sync.Mutex
eventChannels []chan interface{}
}
Listener interface {
OnPause()
OnResume()
}
Poller interface {
Name() string
Poll() string
}
Pusher interface {
Name() string
Push(string) error
}
)
func NewQueue(producerFactory ProducerFactory, consumerFactory ConsumerFactory) *Queue {
queue := &Queue{
metrics: stat.NewMetrics(queueName),
producerFactory: producerFactory,
producerRoutineGroup: threading.NewRoutineGroup(),
consumerFactory: consumerFactory,
consumerRoutineGroup: threading.NewRoutineGroup(),
producerCount: runtime.NumCPU(),
consumerCount: runtime.NumCPU() << 1,
channel: make(chan string),
quit: make(chan struct{}),
}
queue.SetName(queueName)
return queue
}
func (queue *Queue) AddListener(listener Listener) {
queue.listeners = append(queue.listeners, listener)
}
func (queue *Queue) Broadcast(message interface{}) {
go func() {
queue.eventLock.Lock()
defer queue.eventLock.Unlock()
for _, channel := range queue.eventChannels {
channel <- message
}
}()
}
func (queue *Queue) SetName(name string) {
queue.name = name
queue.metrics.SetName(name)
}
func (queue *Queue) SetNumConsumer(count int) {
queue.consumerCount = count
}
func (queue *Queue) SetNumProducer(count int) {
queue.producerCount = count
}
func (queue *Queue) Start() {
queue.startProducers(queue.producerCount)
queue.startConsumers(queue.consumerCount)
queue.producerRoutineGroup.Wait()
close(queue.channel)
queue.consumerRoutineGroup.Wait()
}
func (queue *Queue) Stop() {
close(queue.quit)
}
func (queue *Queue) consume(eventChan chan interface{}) {
var consumer Consumer
for {
var err error
if consumer, err = queue.consumerFactory(); err != nil {
logx.Errorf("Error on creating consumer: %v", err)
time.Sleep(time.Second)
} else {
break
}
}
for {
select {
case message, ok := <-queue.channel:
if ok {
queue.consumeOne(consumer, message)
} else {
logx.Info("Task channel was closed, quitting consumer...")
return
}
case event := <-eventChan:
consumer.OnEvent(event)
}
}
}
func (queue *Queue) consumeOne(consumer Consumer, message string) {
threading.RunSafe(func() {
startTime := timex.Now()
defer func() {
duration := timex.Since(startTime)
queue.metrics.Add(stat.Task{
Duration: duration,
})
logx.WithDuration(duration).Infof("%s", message)
}()
if err := consumer.Consume(message); err != nil {
logx.Errorf("Error occurred while consuming %v: %v", message, err)
}
})
}
func (queue *Queue) pause() {
for _, listener := range queue.listeners {
listener.OnPause()
}
}
func (queue *Queue) produce() {
var producer Producer
for {
var err error
if producer, err = queue.producerFactory(); err != nil {
logx.Errorf("Error on creating producer: %v", err)
time.Sleep(time.Second)
} else {
break
}
}
atomic.AddInt32(&queue.active, 1)
producer.AddListener(routineListener{
queue: queue,
})
for {
select {
case <-queue.quit:
logx.Info("Quitting producer")
return
default:
if v, ok := queue.produceOne(producer); ok {
queue.channel <- v
}
}
}
}
func (queue *Queue) produceOne(producer Producer) (string, bool) {
// avoid panic quit the producer, just log it and continue
defer rescue.Recover()
return producer.Produce()
}
func (queue *Queue) resume() {
for _, listener := range queue.listeners {
listener.OnResume()
}
}
func (queue *Queue) startConsumers(number int) {
for i := 0; i < number; i++ {
eventChan := make(chan interface{})
queue.eventLock.Lock()
queue.eventChannels = append(queue.eventChannels, eventChan)
queue.eventLock.Unlock()
queue.consumerRoutineGroup.Run(func() {
queue.consume(eventChan)
})
}
}
func (queue *Queue) startProducers(number int) {
for i := 0; i < number; i++ {
queue.producerRoutineGroup.Run(func() {
queue.produce()
})
}
}
type routineListener struct {
queue *Queue
}
func (rl routineListener) OnProducerPause() {
if atomic.AddInt32(&rl.queue.active, -1) <= 0 {
rl.queue.pause()
}
}
func (rl routineListener) OnProducerResume() {
if atomic.AddInt32(&rl.queue.active, 1) == 1 {
rl.queue.resume()
}
}

@ -0,0 +1,94 @@
package queue
import (
"sync"
"sync/atomic"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
const (
consumers = 4
rounds = 100
)
func TestQueue(t *testing.T) {
producer := newMockedProducer(rounds)
consumer := newMockedConsumer()
consumer.wait.Add(consumers)
q := NewQueue(func() (Producer, error) {
return producer, nil
}, func() (Consumer, error) {
return consumer, nil
})
q.AddListener(new(mockedListener))
q.SetName("mockqueue")
q.SetNumConsumer(consumers)
q.SetNumProducer(1)
q.pause()
q.resume()
go func() {
producer.wait.Wait()
q.Stop()
}()
q.Start()
assert.Equal(t, int32(rounds), atomic.LoadInt32(&consumer.count))
}
type mockedConsumer struct {
count int32
events int32
wait sync.WaitGroup
}
func newMockedConsumer() *mockedConsumer {
return new(mockedConsumer)
}
func (c *mockedConsumer) Consume(string) error {
atomic.AddInt32(&c.count, 1)
return nil
}
func (c *mockedConsumer) OnEvent(interface{}) {
if atomic.AddInt32(&c.events, 1) <= consumers {
c.wait.Done()
}
}
type mockedProducer struct {
total int32
count int32
wait sync.WaitGroup
}
func newMockedProducer(total int32) *mockedProducer {
p := new(mockedProducer)
p.total = total
p.wait.Add(int(total))
return p
}
func (p *mockedProducer) AddListener(listener ProduceListener) {
}
func (p *mockedProducer) Produce() (string, bool) {
if atomic.AddInt32(&p.count, 1) <= p.total {
p.wait.Done()
return "item", true
} else {
time.Sleep(time.Second)
return "", false
}
}
type mockedListener struct {
}
func (l *mockedListener) OnPause() {
}
func (l *mockedListener) OnResume() {
}

@ -0,0 +1,12 @@
package queue
import "strings"
func generateName(pushers []Pusher) string {
names := make([]string, len(pushers))
for i, pusher := range pushers {
names[i] = pusher.Name()
}
return strings.Join(names, ",")
}

@ -0,0 +1,77 @@
package queue
import (
"errors"
"math"
"testing"
"github.com/stretchr/testify/assert"
"github.com/tal-tech/go-zero/core/logx"
"github.com/tal-tech/go-zero/core/mathx"
)
var (
proba = mathx.NewProba()
failProba = 0.01
)
func init() {
logx.Disable()
}
func TestGenerateName(t *testing.T) {
pushers := []Pusher{
&mockedPusher{name: "first"},
&mockedPusher{name: "second"},
&mockedPusher{name: "third"},
}
assert.Equal(t, "first,second,third", generateName(pushers))
}
func TestGenerateNameNil(t *testing.T) {
var pushers []Pusher
assert.Equal(t, "", generateName(pushers))
}
func calcMean(vals []int) float64 {
if len(vals) == 0 {
return 0
}
var result float64
for _, val := range vals {
result += float64(val)
}
return result / float64(len(vals))
}
func calcVariance(mean float64, vals []int) float64 {
if len(vals) == 0 {
return 0
}
var result float64
for _, val := range vals {
result += math.Pow(float64(val)-mean, 2)
}
return result / float64(len(vals))
}
type mockedPusher struct {
name string
count int
}
func (p *mockedPusher) Name() string {
return p.name
}
func (p *mockedPusher) Push(s string) error {
if proba.TrueOnProba(failProba) {
return errors.New("dummy")
}
p.count++
return nil
}
Loading…
Cancel
Save