github.com/voedger/voedger@v0.0.0-20240520144910-273e84102129/pkg/in10nmem/v1/impl.go (about)

     1  /*
     2   * Copyright (c) 2021-present Sigma-Soft, Ltd.
     3   * Aleksei Ponomarev
     4   */
     5  
     6  package in10nmemv1
     7  
     8  import (
     9  	"context"
    10  	"fmt"
    11  	"sync"
    12  	"time"
    13  
    14  	"github.com/google/uuid"
    15  	"github.com/voedger/voedger/pkg/in10n"
    16  	istructs "github.com/voedger/voedger/pkg/istructs"
    17  )
    18  
    19  type N10nBroker struct {
    20  	projections      map[in10n.ProjectionKey]*istructs.Offset
    21  	channels         map[in10n.ChannelID]*channelType
    22  	quotas           in10n.Quotas
    23  	metricBySubject  map[istructs.SubjectLogin]*metricType
    24  	numSubscriptions int
    25  	now              func() time.Time
    26  	sync.RWMutex
    27  }
    28  
    29  type projectionOffsets struct {
    30  	deliveredOffset istructs.Offset
    31  	currentOffset   *istructs.Offset
    32  }
    33  
    34  type channelType struct {
    35  	subject         istructs.SubjectLogin
    36  	subscriptions   map[in10n.ProjectionKey]*projectionOffsets
    37  	channelDuration time.Duration
    38  	createTime      time.Time
    39  }
    40  
    41  type metricType struct {
    42  	numChannels      int
    43  	numSubscriptions int
    44  }
    45  
    46  // NewChannel @ConcurrentAccess
    47  // Create new channel.
    48  // On timeout channel will be closed. channelDuration determines time during with it will be open.
    49  func (nb *N10nBroker) NewChannel(subject istructs.SubjectLogin, channelDuration time.Duration) (channelID in10n.ChannelID, err error) {
    50  	nb.Lock()
    51  	defer nb.Unlock()
    52  	var metric *metricType
    53  	if len(nb.channels) >= nb.quotas.Channels {
    54  		return "", in10n.ErrQuotaExceeded_Channels
    55  	}
    56  	metric = nb.metricBySubject[subject]
    57  	if metric != nil {
    58  		if metric.numChannels >= nb.quotas.ChannelsPerSubject {
    59  			return "", in10n.ErrQuotaExceeded_ChannelsPerSubject
    60  		}
    61  	} else {
    62  		metric = new(metricType)
    63  		nb.metricBySubject[subject] = metric
    64  	}
    65  	metric.numChannels++
    66  	channelID = in10n.ChannelID(uuid.New().String())
    67  	channel := channelType{
    68  		subject:         subject,
    69  		subscriptions:   make(map[in10n.ProjectionKey]*projectionOffsets),
    70  		channelDuration: channelDuration,
    71  		createTime:      nb.now(),
    72  	}
    73  	nb.channels[channelID] = &channel
    74  	return channelID, err
    75  }
    76  
    77  // Subscribe @ConcurrentAccess
    78  // Subscribe to the channel for the projection. If channel does not exist: will return error ErrChannelNotExists
    79  func (nb *N10nBroker) Subscribe(channelID in10n.ChannelID, projectionKey in10n.ProjectionKey) (err error) {
    80  	nb.Lock()
    81  	defer nb.Unlock()
    82  	channel, channelOK := nb.channels[channelID]
    83  	if !channelOK {
    84  		return in10n.ErrChannelDoesNotExist
    85  	}
    86  
    87  	metric, metricOK := nb.metricBySubject[channel.subject]
    88  	if !metricOK {
    89  		return ErrMetricDoesNotExists
    90  	}
    91  
    92  	if nb.numSubscriptions >= nb.quotas.Subscriptions {
    93  		return in10n.ErrQuotaExceeded_Subsciptions
    94  	}
    95  	if metric.numSubscriptions >= nb.quotas.SubscriptionsPerSubject {
    96  		return in10n.ErrQuotaExceeded_SubsciptionsPerSubject
    97  	}
    98  
    99  	subscription := projectionOffsets{
   100  		deliveredOffset: istructs.Offset(0),
   101  		currentOffset:   guaranteeOffsetPointer(nb.projections, projectionKey),
   102  	}
   103  	channel.subscriptions[projectionKey] = &subscription
   104  	metric.numSubscriptions++
   105  	nb.numSubscriptions++
   106  	return err
   107  }
   108  
   109  func (nb *N10nBroker) Unsubscribe(channelID in10n.ChannelID, projection in10n.ProjectionKey) (err error) {
   110  	nb.Lock()
   111  	defer nb.Unlock()
   112  
   113  	channel, cOK := nb.channels[channelID]
   114  	if !cOK {
   115  		return in10n.ErrChannelDoesNotExist
   116  	}
   117  	metric, mOK := nb.metricBySubject[channel.subject]
   118  	if !mOK {
   119  		return ErrMetricDoesNotExists
   120  	}
   121  	delete(channel.subscriptions, projection)
   122  	metric.numSubscriptions--
   123  	nb.numSubscriptions--
   124  	return err
   125  }
   126  
   127  // WatchChannel @ConcurrentAccess
   128  // Create WatchChannel for notify clients about changed projections. If channel for this demand does not exist or
   129  // channel already watched - exit.
   130  func (nb *N10nBroker) WatchChannel(ctx context.Context, channelID in10n.ChannelID, notifySubscriber func(projection in10n.ProjectionKey, offset istructs.Offset)) {
   131  	// check that the channelID with the given ChannelID exists
   132  	channel, metric := func() (*channelType, *metricType) {
   133  		nb.RLock()
   134  		defer nb.RUnlock()
   135  		channel, channelOK := nb.channels[channelID]
   136  		if !channelOK {
   137  			panic(fmt.Errorf("channel with channelID: %s must exists %w", channelID, in10n.ErrChannelDoesNotExist))
   138  		}
   139  		metric, metricOK := nb.metricBySubject[channel.subject]
   140  		if !metricOK {
   141  			panic(fmt.Errorf("metric for channel with channelID: %s must exists", channelID))
   142  		}
   143  		return channel, metric
   144  	}()
   145  
   146  	defer func() {
   147  		nb.Lock()
   148  		metric.numChannels--
   149  		metric.numSubscriptions -= len(channel.subscriptions)
   150  		nb.numSubscriptions -= len(channel.subscriptions)
   151  		delete(nb.channels, channelID)
   152  		nb.Unlock()
   153  	}()
   154  
   155  	ticker := time.NewTicker(pollingInterval)
   156  	defer ticker.Stop()
   157  
   158  	updateUnits := make([]UpdateUnit, 0)
   159  	for range ticker.C {
   160  
   161  		if ctx.Err() != nil {
   162  			return
   163  		}
   164  
   165  		err := nb.validateChannel(channel)
   166  		if err != nil {
   167  			return
   168  		}
   169  
   170  		// find projection for update and collect
   171  		nb.Lock()
   172  		for projection, channelOffsets := range channel.subscriptions {
   173  			if *channelOffsets.currentOffset > channelOffsets.deliveredOffset {
   174  				updateUnits = append(updateUnits,
   175  					UpdateUnit{
   176  						Projection: projection,
   177  						Offset:     *channelOffsets.currentOffset,
   178  					})
   179  				channelOffsets.deliveredOffset = *channelOffsets.currentOffset
   180  			}
   181  		}
   182  		nb.Unlock()
   183  		for _, unit := range updateUnits {
   184  			notifySubscriber(unit.Projection, unit.Offset)
   185  		}
   186  		updateUnits = updateUnits[:0]
   187  	}
   188  }
   189  
   190  func guaranteeOffsetPointer(projections map[in10n.ProjectionKey]*istructs.Offset, projection in10n.ProjectionKey) (offsetPointer *istructs.Offset) {
   191  	offsetPointer = projections[projection]
   192  	if offsetPointer == nil {
   193  		offsetPointer = new(istructs.Offset)
   194  		projections[projection] = offsetPointer
   195  	}
   196  	return
   197  }
   198  
   199  // Update @ConcurrentAccess
   200  // Update projections map with new offset
   201  func (nb *N10nBroker) Update(projection in10n.ProjectionKey, offset istructs.Offset) {
   202  	nb.Lock()
   203  	defer nb.Unlock()
   204  	*guaranteeOffsetPointer(nb.projections, projection) = offset
   205  }
   206  
   207  // MetricNumChannels @ConcurrentAccess
   208  // return channels count
   209  func (nb *N10nBroker) MetricNumChannels() int {
   210  	nb.RLock()
   211  	defer nb.RUnlock()
   212  	return len(nb.channels)
   213  }
   214  
   215  func (nb *N10nBroker) MetricNumSubcriptions() int {
   216  	nb.RLock()
   217  	defer nb.RUnlock()
   218  	return nb.numSubscriptions
   219  }
   220  
   221  func (nb *N10nBroker) MetricSubject(ctx context.Context, cb func(subject istructs.SubjectLogin, numChannels int, numSubscriptions int)) {
   222  	postMetric := func(subject istructs.SubjectLogin, metric *metricType) (err error) {
   223  		nb.RLock()
   224  		defer nb.RUnlock()
   225  		cb(subject, metric.numChannels, metric.numSubscriptions)
   226  		return err
   227  	}
   228  	for subject, subjectMetric := range nb.metricBySubject {
   229  		if ctx.Err() != nil {
   230  			return
   231  		}
   232  		err := postMetric(subject, subjectMetric)
   233  		if err != nil {
   234  			return
   235  		}
   236  	}
   237  }
   238  
   239  func NewN10nBroker(quotas in10n.Quotas, now func() time.Time) *N10nBroker {
   240  	broker := N10nBroker{
   241  		projections:     make(map[in10n.ProjectionKey]*istructs.Offset),
   242  		channels:        make(map[in10n.ChannelID]*channelType),
   243  		metricBySubject: make(map[istructs.SubjectLogin]*metricType),
   244  		quotas:          quotas,
   245  		now:             now,
   246  	}
   247  	return &broker
   248  }
   249  
   250  func (nb *N10nBroker) validateChannel(channel *channelType) error {
   251  	nb.RLock()
   252  	defer nb.RUnlock()
   253  	// if channel lifetime > channelDuration defined in NewChannel when create channel - must exit
   254  	if nb.Since(channel.createTime) > channel.channelDuration {
   255  		return ErrChannelExpired
   256  	}
   257  	return nil
   258  }
   259  
   260  func (nb *N10nBroker) Since(t time.Time) time.Duration {
   261  	return nb.now().Sub(t)
   262  }