1
0
mirror of https://github.com/v2fly/v2ray-core.git synced 2024-12-21 09:36:34 -05:00

Merge pull request #219 from Vigilans/vigilans/stats-channel-config

Stats: Add ChannelConfig & Return error on subscription
This commit is contained in:
Loyalsoldier 2020-09-24 02:21:45 +08:00 committed by GitHub
commit 788dd1ebc7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 267 additions and 116 deletions

View File

@ -5,15 +5,33 @@ package stats
import (
"sync"
"time"
"v2ray.com/core/common"
)
// Channel is an implementation of stats.Channel.
type Channel struct {
channel chan interface{}
subscribers []chan interface{}
// Synchronization components
access sync.RWMutex
closed chan struct{}
channel chan interface{}
subscribers []chan interface{}
// Channel options
subscriberLimit int // Set to 0 as no subscriber limit
channelBufferSize int // Set to 0 as no buffering
broadcastTimeout time.Duration // Set to 0 as non-blocking immediate timeout
}
// NewChannel creates an instance of Statistics Channel.
func NewChannel(config *ChannelConfig) *Channel {
return &Channel{
channel: make(chan interface{}, config.BufferSize),
subscriberLimit: int(config.SubscriberLimit),
channelBufferSize: int(config.BufferSize),
broadcastTimeout: time.Duration(config.BroadcastTimeout+1) * time.Millisecond,
}
}
// Channel returns the underlying go channel.
@ -31,16 +49,19 @@ func (c *Channel) Subscribers() []chan interface{} {
}
// Subscribe implements stats.Channel.
func (c *Channel) Subscribe() chan interface{} {
func (c *Channel) Subscribe() (chan interface{}, error) {
c.access.Lock()
defer c.access.Unlock()
subscriber := make(chan interface{})
if c.subscriberLimit > 0 && len(c.subscribers) >= c.subscriberLimit {
return nil, newError("Number of subscribers has reached limit")
}
subscriber := make(chan interface{}, c.channelBufferSize)
c.subscribers = append(c.subscribers, subscriber)
return subscriber
return subscriber, nil
}
// Unsubscribe implements stats.Channel.
func (c *Channel) Unsubscribe(subscriber chan interface{}) {
func (c *Channel) Unsubscribe(subscriber chan interface{}) error {
c.access.Lock()
defer c.access.Unlock()
for i, s := range c.subscribers {
@ -50,9 +71,9 @@ func (c *Channel) Unsubscribe(subscriber chan interface{}) {
copy(subscribers[:i], c.subscribers[:i])
copy(subscribers[i:], c.subscribers[i+1:])
c.subscribers = subscribers
return
}
}
return nil
}
// Publish implements stats.Channel.
@ -85,34 +106,30 @@ func (c *Channel) Running() bool {
func (c *Channel) Start() error {
c.access.Lock()
defer c.access.Unlock()
if c.Running() {
return nil
}
if c.channel == nil { // Initialize publisher channel
c.channel = make(chan interface{}, 16)
}
c.closed = make(chan struct{}) // Reset close signal
go func() {
for {
select {
case message := <-c.channel: // Broadcast message
for _, sub := range c.Subscribers() { // Concurrency-safe subscribers retreivement
select {
case sub <- message: // Successfully sent message
case <-time.After(100 * time.Millisecond):
c.Unsubscribe(sub) // Remove timeout subscriber
close(sub) // Actively close subscriber as notification
if !c.Running() {
c.closed = make(chan struct{}) // Reset close signal
go func() {
for {
select {
case message := <-c.channel: // Broadcast message
for _, sub := range c.Subscribers() { // Concurrency-safe subscribers retreivement
select {
case sub <- message: // Successfully sent message
case <-time.After(c.broadcastTimeout): // Remove timeout subscriber
common.Must(c.Unsubscribe(sub))
close(sub) // Actively close subscriber as notification
}
}
case <-c.closed: // Channel closed
for _, sub := range c.Subscribers() { // Remove all subscribers
common.Must(c.Unsubscribe(sub))
close(sub)
}
return
}
case <-c.closed: // Channel closed
for _, sub := range c.Subscribers() { // Remove all subscribers
c.Unsubscribe(sub)
close(sub)
}
return
}
}
}()
}()
}
return nil
}

View File

@ -1,7 +1,6 @@
package stats_test
import (
"context"
"fmt"
"testing"
"time"
@ -12,25 +11,30 @@ import (
)
func TestStatsChannel(t *testing.T) {
raw, err := common.CreateObject(context.Background(), &Config{})
// At most 2 subscribers could be registered
c := NewChannel(&ChannelConfig{SubscriberLimit: 2})
source := c.Channel()
a, err := stats.SubscribeRunnableChannel(c)
common.Must(err)
if !c.Running() {
t.Fatal("unexpected failure in running channel after first subscription")
}
b, err := c.Subscribe()
common.Must(err)
m := raw.(stats.Manager)
c, err := m.RegisterChannel("test.channel")
common.Must(err)
common.Must(m.Start())
defer m.Close()
source := c.(*Channel).Channel()
a := c.Subscribe()
b := c.Subscribe()
defer c.Unsubscribe(a)
defer c.Unsubscribe(b)
// Test that third subscriber is forbidden
_, err = c.Subscribe()
if err == nil {
t.Fatal("unexpected successful subscription")
}
t.Log("expected error: ", err)
stopCh := make(chan struct{})
errCh := make(chan string)
go func() {
go func() { // Blocking publish
source <- 1
source <- 2
source <- "3"
@ -84,22 +88,31 @@ func TestStatsChannel(t *testing.T) {
t.Fatal(e)
case <-stopCh:
}
// Test the unsubscription of channel
common.Must(c.Unsubscribe(b))
// Test the last subscriber will close channel with `UnsubscribeClosableChannel`
common.Must(stats.UnsubscribeClosableChannel(c, a))
if c.Running() {
t.Fatal("unexpected running channel after unsubscribing the last subscriber")
}
}
func TestStatsChannelUnsubcribe(t *testing.T) {
raw, err := common.CreateObject(context.Background(), &Config{})
common.Must(err)
c := NewChannel(&ChannelConfig{})
common.Must(c.Start())
defer c.Close()
m := raw.(stats.Manager)
c, err := m.RegisterChannel("test.channel")
common.Must(err)
common.Must(m.Start())
defer m.Close()
source := c.Channel()
a := c.Subscribe()
b := c.Subscribe()
a, err := c.Subscribe()
common.Must(err)
defer c.Unsubscribe(a)
b, err := c.Subscribe()
common.Must(err)
pauseCh := make(chan struct{})
stopCh := make(chan struct{})
errCh := make(chan string)
@ -119,10 +132,10 @@ func TestStatsChannelUnsubcribe(t *testing.T) {
}
}
go func() {
c.Publish(1)
go func() { // Blocking publish
source <- 1
<-pauseCh // Wait for `b` goroutine to resume sending message
c.Publish(2)
source <- 2
}()
go func() {
@ -179,26 +192,27 @@ func TestStatsChannelUnsubcribe(t *testing.T) {
}
func TestStatsChannelTimeout(t *testing.T) {
raw, err := common.CreateObject(context.Background(), &Config{})
common.Must(err)
// Do not use buffer so as to create blocking scenario
c := NewChannel(&ChannelConfig{BufferSize: 0, BroadcastTimeout: 50})
common.Must(c.Start())
defer c.Close()
m := raw.(stats.Manager)
c, err := m.RegisterChannel("test.channel")
common.Must(err)
common.Must(m.Start())
defer m.Close()
source := c.Channel()
a := c.Subscribe()
b := c.Subscribe()
a, err := c.Subscribe()
common.Must(err)
defer c.Unsubscribe(a)
b, err := c.Subscribe()
common.Must(err)
defer c.Unsubscribe(b)
stopCh := make(chan struct{})
errCh := make(chan string)
go func() {
c.Publish(1)
c.Publish(2)
go func() { // Blocking publish
source <- 1
source <- 2
}()
go func() {
@ -229,7 +243,7 @@ func TestStatsChannelTimeout(t *testing.T) {
errCh <- fmt.Sprint("unexpected receiving: ", v, ", wanted ", 1)
}
// Block `b` channel for a time longer than `source`'s timeout
<-time.After(150 * time.Millisecond)
<-time.After(200 * time.Millisecond)
{ // Test `b` has been unsubscribed by source
var aSet, bSet bool
for _, s := range c.Subscribers() {
@ -264,25 +278,27 @@ func TestStatsChannelTimeout(t *testing.T) {
}
func TestStatsChannelConcurrency(t *testing.T) {
raw, err := common.CreateObject(context.Background(), &Config{})
common.Must(err)
// Do not use buffer so as to create blocking scenario
c := NewChannel(&ChannelConfig{BufferSize: 0, BroadcastTimeout: 100})
common.Must(c.Start())
defer c.Close()
m := raw.(stats.Manager)
c, err := m.RegisterChannel("test.channel")
common.Must(err)
common.Must(m.Start())
defer m.Close()
source := c.Channel()
a := c.Subscribe()
b := c.Subscribe()
a, err := c.Subscribe()
common.Must(err)
defer c.Unsubscribe(a)
b, err := c.Subscribe()
common.Must(err)
defer c.Unsubscribe(b)
stopCh := make(chan struct{})
errCh := make(chan string)
go func() {
c.Publish(1)
c.Publish(2)
go func() { // Blocking publish
source <- 1
source <- 2
}()
go func() {

View File

@ -1,15 +0,0 @@
// +build !confonly
package stats
import (
"context"
"v2ray.com/core/common"
)
func init() {
common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, config interface{}) (interface{}, error) {
return NewManager(ctx, config.(*Config))
}))
}

View File

@ -63,18 +63,90 @@ func (*Config) Descriptor() ([]byte, []int) {
return file_app_stats_config_proto_rawDescGZIP(), []int{0}
}
type ChannelConfig struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
SubscriberLimit int32 `protobuf:"varint,1,opt,name=SubscriberLimit,proto3" json:"SubscriberLimit,omitempty"`
BufferSize int32 `protobuf:"varint,2,opt,name=BufferSize,proto3" json:"BufferSize,omitempty"`
BroadcastTimeout int32 `protobuf:"varint,3,opt,name=BroadcastTimeout,proto3" json:"BroadcastTimeout,omitempty"`
}
func (x *ChannelConfig) Reset() {
*x = ChannelConfig{}
if protoimpl.UnsafeEnabled {
mi := &file_app_stats_config_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *ChannelConfig) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ChannelConfig) ProtoMessage() {}
func (x *ChannelConfig) ProtoReflect() protoreflect.Message {
mi := &file_app_stats_config_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ChannelConfig.ProtoReflect.Descriptor instead.
func (*ChannelConfig) Descriptor() ([]byte, []int) {
return file_app_stats_config_proto_rawDescGZIP(), []int{1}
}
func (x *ChannelConfig) GetSubscriberLimit() int32 {
if x != nil {
return x.SubscriberLimit
}
return 0
}
func (x *ChannelConfig) GetBufferSize() int32 {
if x != nil {
return x.BufferSize
}
return 0
}
func (x *ChannelConfig) GetBroadcastTimeout() int32 {
if x != nil {
return x.BroadcastTimeout
}
return 0
}
var File_app_stats_config_proto protoreflect.FileDescriptor
var file_app_stats_config_proto_rawDesc = []byte{
0x0a, 0x16, 0x61, 0x70, 0x70, 0x2f, 0x73, 0x74, 0x61, 0x74, 0x73, 0x2f, 0x63, 0x6f, 0x6e, 0x66,
0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x14, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e,
0x63, 0x6f, 0x72, 0x65, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x73, 0x74, 0x61, 0x74, 0x73, 0x22, 0x08,
0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x42, 0x4d, 0x0a, 0x18, 0x63, 0x6f, 0x6d, 0x2e,
0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x73,
0x74, 0x61, 0x74, 0x73, 0x50, 0x01, 0x5a, 0x18, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f,
0x6d, 0x2f, 0x63, 0x6f, 0x72, 0x65, 0x2f, 0x61, 0x70, 0x70, 0x2f, 0x73, 0x74, 0x61, 0x74, 0x73,
0xaa, 0x02, 0x14, 0x56, 0x32, 0x52, 0x61, 0x79, 0x2e, 0x43, 0x6f, 0x72, 0x65, 0x2e, 0x41, 0x70,
0x70, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x85, 0x01, 0x0a, 0x0d, 0x43, 0x68, 0x61,
0x6e, 0x6e, 0x65, 0x6c, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x28, 0x0a, 0x0f, 0x53, 0x75,
0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x72, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x01, 0x20,
0x01, 0x28, 0x05, 0x52, 0x0f, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x72, 0x4c,
0x69, 0x6d, 0x69, 0x74, 0x12, 0x1e, 0x0a, 0x0a, 0x42, 0x75, 0x66, 0x66, 0x65, 0x72, 0x53, 0x69,
0x7a, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0a, 0x42, 0x75, 0x66, 0x66, 0x65, 0x72,
0x53, 0x69, 0x7a, 0x65, 0x12, 0x2a, 0x0a, 0x10, 0x42, 0x72, 0x6f, 0x61, 0x64, 0x63, 0x61, 0x73,
0x74, 0x54, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x10,
0x42, 0x72, 0x6f, 0x61, 0x64, 0x63, 0x61, 0x73, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74,
0x42, 0x4d, 0x0a, 0x18, 0x63, 0x6f, 0x6d, 0x2e, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f,
0x72, 0x65, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x73, 0x74, 0x61, 0x74, 0x73, 0x50, 0x01, 0x5a, 0x18,
0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x72, 0x65, 0x2f, 0x61,
0x70, 0x70, 0x2f, 0x73, 0x74, 0x61, 0x74, 0x73, 0xaa, 0x02, 0x14, 0x56, 0x32, 0x52, 0x61, 0x79,
0x2e, 0x43, 0x6f, 0x72, 0x65, 0x2e, 0x41, 0x70, 0x70, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x73, 0x62,
0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
@ -89,9 +161,10 @@ func file_app_stats_config_proto_rawDescGZIP() []byte {
return file_app_stats_config_proto_rawDescData
}
var file_app_stats_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
var file_app_stats_config_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
var file_app_stats_config_proto_goTypes = []interface{}{
(*Config)(nil), // 0: v2ray.core.app.stats.Config
(*Config)(nil), // 0: v2ray.core.app.stats.Config
(*ChannelConfig)(nil), // 1: v2ray.core.app.stats.ChannelConfig
}
var file_app_stats_config_proto_depIdxs = []int32{
0, // [0:0] is the sub-list for method output_type
@ -119,6 +192,18 @@ func file_app_stats_config_proto_init() {
return nil
}
}
file_app_stats_config_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*ChannelConfig); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
@ -126,7 +211,7 @@ func file_app_stats_config_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_app_stats_config_proto_rawDesc,
NumEnums: 0,
NumMessages: 1,
NumMessages: 2,
NumExtensions: 0,
NumServices: 0,
},

View File

@ -7,5 +7,11 @@ option java_package = "com.v2ray.core.app.stats";
option java_multiple_files = true;
message Config {
}
message ChannelConfig {
int32 SubscriberLimit = 1;
int32 BufferSize = 2;
int32 BroadcastTimeout = 3;
}

View File

@ -8,6 +8,8 @@ import (
"context"
"sync"
"v2ray.com/core/common"
"v2ray.com/core/common/errors"
"v2ray.com/core/features/stats"
)
@ -92,10 +94,10 @@ func (m *Manager) RegisterChannel(name string) (stats.Channel, error) {
return nil, newError("Channel ", name, " already registered.")
}
newError("create new channel ", name).AtDebug().WriteToLog()
c := new(Channel)
c := NewChannel(&ChannelConfig{BufferSize: 16, BroadcastTimeout: 100})
m.channels[name] = c
if m.running {
c.Start()
return c, c.Start()
}
return c, nil
}
@ -108,7 +110,7 @@ func (m *Manager) UnregisterChannel(name string) error {
if c, found := m.channels[name]; found {
newError("remove channel ", name).AtDebug().WriteToLog()
delete(m.channels, name)
c.Close()
return c.Close()
}
return nil
}
@ -129,8 +131,14 @@ func (m *Manager) Start() error {
m.access.Lock()
defer m.access.Unlock()
m.running = true
errs := []error{}
for _, channel := range m.channels {
channel.Start()
if err := channel.Start(); err != nil {
errs = append(errs, err)
}
}
if len(errs) != 0 {
return errors.Combine(errs...)
}
return nil
}
@ -140,10 +148,22 @@ func (m *Manager) Close() error {
m.access.Lock()
defer m.access.Unlock()
m.running = false
errs := []error{}
for name, channel := range m.channels {
newError("remove channel ", name).AtDebug().WriteToLog()
delete(m.channels, name)
channel.Close()
if err := channel.Close(); err != nil {
errs = append(errs, err)
}
}
if len(errs) != 0 {
return errors.Combine(errs...)
}
return nil
}
func init() {
common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, config interface{}) (interface{}, error) {
return NewManager(ctx, config.(*Config))
}))
}

View File

@ -42,7 +42,8 @@ func TestStatsChannelRunnable(t *testing.T) {
t.Fatalf("unexpected non-running channel: test.channel.%d", 2)
}
s1 := c1.Subscribe()
s1, err := c1.Subscribe()
common.Must(err)
common.Must(c1.Close())
if c1.Running() {

View File

@ -30,9 +30,30 @@ type Channel interface {
// SubscriberCount returns the number of the subscribers.
Subscribers() []chan interface{}
// Subscribe registers for listening to channel stream and returns a new listener channel.
Subscribe() chan interface{}
Subscribe() (chan interface{}, error)
// Unsubscribe unregisters a listener channel from current Channel object.
Unsubscribe(chan interface{})
Unsubscribe(chan interface{}) error
}
// SubscribeRunnableChannel subscribes the channel and starts it if there is first subscriber coming.
func SubscribeRunnableChannel(c Channel) (chan interface{}, error) {
if len(c.Subscribers()) == 0 {
if err := c.Start(); err != nil {
return nil, err
}
}
return c.Subscribe()
}
// UnsubscribeClosableChannel unsubcribes the channel and close it if there is no more subscriber.
func UnsubscribeClosableChannel(c Channel, sub chan interface{}) error {
if err := c.Unsubscribe(sub); err != nil {
return err
}
if len(c.Subscribers()) == 0 {
return c.Close()
}
return nil
}
// Manager is the interface for stats manager.