github.com/altipla-consulting/ravendb-go-client@v0.1.3/database_changes.go (about)

     1  package ravendb
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"errors"
     7  	"fmt"
     8  	"reflect"
     9  	"strings"
    10  	"sync"
    11  	"sync/atomic"
    12  	"time"
    13  
    14  	"github.com/gorilla/websocket"
    15  )
    16  
    17  // In Java it's hidden behind IDatabaseChanges which also contains IConnectableChanges
    18  
    19  // Note: in Java IChangesConnectionState hides changeSubscribers
    20  
    21  // enableDatabaseChangesDebugOutput enables debug logging of
    22  // database_changes.go code
    23  var enableDatabaseChangesDebugOutput bool
    24  
    25  // for debugging DatabaseChanges code
    26  func dcdbg(format string, args ...interface{}) {
    27  	// change to true to enable debug output
    28  	if enableDatabaseChangesDebugOutput {
    29  		fmt.Printf(format, args...)
    30  	}
    31  }
    32  
    33  type changeSubscribers struct {
    34  	name           string // is key of DatabaseChanges.subscribers
    35  	watchCommand   string
    36  	unwatchCommand string
    37  	commandValue   string
    38  
    39  	onDocumentChange        sync.Map // int -> func(*DocumentChange)
    40  	onIndexChange           sync.Map // int -> func(*IndexChange)
    41  	onOperationStatusChange sync.Map // int -> func(*OperationStatusChange)
    42  
    43  	nextID int32 // atomic
    44  }
    45  
    46  func (s *changeSubscribers) getNextID() int {
    47  	n := atomic.AddInt32(&s.nextID, 1)
    48  	return int(n)
    49  }
    50  
    51  func (s *changeSubscribers) registerOnDocumentChange(fn func(*DocumentChange)) int {
    52  	id := s.getNextID()
    53  	s.onDocumentChange.Store(id, fn)
    54  	return id
    55  }
    56  
    57  func (s *changeSubscribers) unregisterOnDocumentChange(id int) {
    58  	s.onDocumentChange.Delete(id)
    59  }
    60  
    61  func (s *changeSubscribers) registerOnIndexChange(fn func(*IndexChange)) int {
    62  	id := s.getNextID()
    63  	s.onIndexChange.Store(id, fn)
    64  	return id
    65  }
    66  
    67  func (s *changeSubscribers) unregisterOnIndexChange(id int) {
    68  	s.onIndexChange.Delete(id)
    69  }
    70  
    71  func (s *changeSubscribers) registerOnOperationStatusChange(fn func(*OperationStatusChange)) int {
    72  	id := s.getNextID()
    73  	s.onOperationStatusChange.Store(id, fn)
    74  	return id
    75  }
    76  
    77  func (s *changeSubscribers) unregisterOnOperationStatusChange(id int) {
    78  	s.onOperationStatusChange.Delete(id)
    79  }
    80  
    81  func (s *changeSubscribers) sendDocumentChange(change *DocumentChange) {
    82  	s.onDocumentChange.Range(func(k, v interface{}) bool {
    83  		f := v.(func(documentChange *DocumentChange))
    84  		f(change)
    85  		return true
    86  	})
    87  }
    88  
    89  func (s *changeSubscribers) sendIndexChange(change *IndexChange) {
    90  	s.onIndexChange.Range(func(k, v interface{}) bool {
    91  		f := v.(func(documentChange *IndexChange))
    92  		f(change)
    93  		return true
    94  	})
    95  }
    96  
    97  func (s *changeSubscribers) sendOperationStatusChange(change *OperationStatusChange) {
    98  	s.onOperationStatusChange.Range(func(k, v interface{}) bool {
    99  		f := v.(func(documentChange *OperationStatusChange))
   100  		f(change)
   101  		return true
   102  	})
   103  }
   104  
   105  func (s *changeSubscribers) hasRegisteredHandlers() bool {
   106  	// there is no sync.Map.Count() so we have to enumerate to see
   107  	// if there are any registered handlers
   108  	hasHandlers := false
   109  	fn := func(k, v interface{}) bool {
   110  		hasHandlers = true
   111  		// only care about the first one
   112  		return false
   113  	}
   114  	s.onDocumentChange.Range(fn)
   115  	s.onIndexChange.Range(fn)
   116  	s.onOperationStatusChange.Range(fn)
   117  	return hasHandlers
   118  }
   119  
   120  func newDatabaseChangesCommand(id int, command string, value string) *databaseChangesCommand {
   121  	return &databaseChangesCommand{
   122  		id:        id,
   123  		command:   command,
   124  		value:     value,
   125  		timeStart: time.Now(),
   126  		ch:        make(chan bool, 1), // don't block the sender
   127  	}
   128  }
   129  
   130  type databaseChangesCommand struct {
   131  	// the data we send
   132  	id      int
   133  	command string
   134  	value   string
   135  
   136  	// used to wait for notifications
   137  	timeStart    time.Time
   138  	duration     time.Duration
   139  	ch           chan bool
   140  	completed    int32 // atomic
   141  	wasCancelled bool
   142  }
   143  
   144  func (c *databaseChangesCommand) confirm(wasCancelled bool) {
   145  	v := atomic.AddInt32(&c.completed, 1)
   146  	if v > 1 {
   147  		// was already completed
   148  		return
   149  	}
   150  	c.duration = time.Since(c.timeStart)
   151  	c.wasCancelled = wasCancelled
   152  	c.ch <- true
   153  }
   154  
   155  func (c *databaseChangesCommand) waitForConfirmation(timeout time.Duration) bool {
   156  	select {
   157  	case <-c.ch:
   158  		return true
   159  	case <-time.After(timeout):
   160  		return false
   161  	}
   162  }
   163  
   164  // DatabaseChanges notifies about changes to a database
   165  type DatabaseChanges struct {
   166  	commandID           int32 // atomic
   167  	connStatusChangedID int32 // atomic
   168  
   169  	requestExecutor *RequestExecutor
   170  	conventions     *DocumentConventions
   171  	database        string
   172  
   173  	onClose func()
   174  
   175  	ctxCancel    context.Context
   176  	doWorkCancel context.CancelFunc
   177  
   178  	// will be notified if we connect or fail to connect
   179  	// allows waiting for connection being established
   180  	chIsConnected chan error
   181  
   182  	chCommands      chan *databaseChangesCommand
   183  	chWorkCompleted chan error
   184  
   185  	subscribers sync.Map // string => *changeSubscribers
   186  
   187  	mu sync.Mutex
   188  
   189  	// commands that have been sent to the server but not confirmed
   190  	outstandingCommands sync.Map // int -> *commandConfirmation
   191  
   192  	// TODO: why is this not used?
   193  	// immediateConnection int32 // atomic
   194  
   195  	connectionStatusChanged []func()
   196  	onError                 []func(error)
   197  
   198  	lastError atomic.Value // error
   199  }
   200  
   201  func (c *DatabaseChanges) isClosed() bool {
   202  	select {
   203  	case <-c.ctxCancel.Done():
   204  		return true
   205  	default:
   206  		return false
   207  	}
   208  }
   209  
   210  func (c *DatabaseChanges) nextCommandID() int {
   211  	v := atomic.AddInt32(&c.commandID, 1)
   212  	return int(v)
   213  }
   214  
   215  func newDatabaseChanges(requestExecutor *RequestExecutor, databaseName string, onClose func()) *DatabaseChanges {
   216  	res := &DatabaseChanges{
   217  		requestExecutor: requestExecutor,
   218  		conventions:     requestExecutor.GetConventions(),
   219  		database:        databaseName,
   220  		chIsConnected:   make(chan error, 1),
   221  		onClose:         onClose,
   222  		chWorkCompleted: make(chan error, 1),
   223  		chCommands:      make(chan *databaseChangesCommand, 32),
   224  	}
   225  
   226  	res.ctxCancel, res.doWorkCancel = context.WithCancel(context.Background())
   227  
   228  	go func() {
   229  		_, err := requestExecutor.getPreferredNode()
   230  		if err != nil {
   231  			dcdbg("newDatabaseChanges: getPreferredNode() failed with %s\n", err)
   232  			res.notifyAboutError(err)
   233  			res.chWorkCompleted <- err
   234  			close(res.chWorkCompleted)
   235  			return
   236  		}
   237  
   238  		err = res.doWork(res.ctxCancel)
   239  		res.chWorkCompleted <- err
   240  		close(res.chWorkCompleted)
   241  	}()
   242  
   243  	return res
   244  }
   245  
   246  func (c *DatabaseChanges) EnsureConnectedNow() error {
   247  	select {
   248  	case <-c.ctxCancel.Done():
   249  		dcdbg("DatabaseChanges(): EnsureConnectedNow(): is closed\n")
   250  		return errors.New("DatabaseChanges.EnsureConnectedNow(): Close() has been called")
   251  	case err := <-c.chWorkCompleted:
   252  		dcdbg("DatabaseChanges(): EnsureConnectedNow(): chnWorkCompleted notified\n")
   253  		return err
   254  	case err := <-c.chIsConnected:
   255  		dcdbg("DatabaseChanges(): EnsureConnectedNow(): chanIsConnected notified\n")
   256  		return err
   257  	case <-time.After(time.Second * 15):
   258  		dcdbg("DatabaseChanges(): EnsureConnectedNow(): timed out waiting for connection\n")
   259  		return errors.New("timed out waiting for connection")
   260  	}
   261  }
   262  
   263  func (c *DatabaseChanges) AddConnectionStatusChanged(handler func()) int {
   264  	c.mu.Lock()
   265  	idx := len(c.connectionStatusChanged)
   266  	c.connectionStatusChanged = append(c.connectionStatusChanged, handler)
   267  	c.mu.Unlock()
   268  	return idx
   269  }
   270  
   271  func (c *DatabaseChanges) RemoveConnectionStatusChanged(handlerID int) {
   272  	if handlerID != -1 {
   273  		c.mu.Lock()
   274  		c.connectionStatusChanged[handlerID] = nil
   275  		c.mu.Unlock()
   276  	}
   277  }
   278  
   279  type CancelFunc func()
   280  
   281  // ForIndex registers a callback that will be called for changes in an index with a given name.
   282  // It returns a function to call to unregister the callback.
   283  func (c *DatabaseChanges) ForIndex(indexName string, cb func(*IndexChange)) (CancelFunc, error) {
   284  	subscribers, err := c.getOrAddSubscribers("indexes/"+indexName, "watch-index", "unwatch-index", indexName)
   285  	if err != nil {
   286  		return nil, err
   287  	}
   288  
   289  	filtered := func(change *IndexChange) {
   290  		if strings.EqualFold(change.Name, indexName) {
   291  			cb(change)
   292  		}
   293  	}
   294  	idx := subscribers.registerOnIndexChange(filtered)
   295  	cancel := func() {
   296  		subscribers.unregisterOnIndexChange(idx)
   297  		c.maybeDisconnectSubscribers(subscribers)
   298  	}
   299  
   300  	return cancel, nil
   301  }
   302  
   303  func (c *DatabaseChanges) getLastConnectionStateError() error {
   304  	if v := c.lastError.Load(); v == nil {
   305  		return nil
   306  	} else {
   307  		return v.(error)
   308  	}
   309  }
   310  
   311  // ForDocument registers a callback that will be called for changes on a ocument with a given id
   312  // It returns a function to call to unregister the callback.
   313  func (c *DatabaseChanges) ForDocument(docID string, cb func(*DocumentChange)) (CancelFunc, error) {
   314  	subscribers, err := c.getOrAddSubscribers("docs/"+docID, "watch-doc", "unwatch-doc", docID)
   315  	if err != nil {
   316  		return nil, err
   317  	}
   318  
   319  	filtered := func(change *DocumentChange) {
   320  		panicIf(change.ID != docID, "v.ID (%s) != docID (%s)", change.ID, docID)
   321  		cb(change)
   322  	}
   323  	idx := subscribers.registerOnDocumentChange(filtered)
   324  	cancel := func() {
   325  		subscribers.unregisterOnDocumentChange(idx)
   326  		c.maybeDisconnectSubscribers(subscribers)
   327  	}
   328  	return cancel, nil
   329  }
   330  
   331  // ForAllDocuments registers a callback that will be called for changes on all documents.
   332  // It returns a function to call to unregister the callback.
   333  func (c *DatabaseChanges) ForAllDocuments(cb func(*DocumentChange)) (CancelFunc, error) {
   334  	subscribers, err := c.getOrAddSubscribers("all-docs", "watch-docs", "unwatch-docs", "")
   335  	if err != nil {
   336  		return nil, err
   337  	}
   338  
   339  	idx := subscribers.registerOnDocumentChange(cb)
   340  	cancel := func() {
   341  		subscribers.unregisterOnDocumentChange(idx)
   342  		c.maybeDisconnectSubscribers(subscribers)
   343  	}
   344  	return cancel, nil
   345  }
   346  
   347  // ForOperationID registers a callback that will be called when a change happens to operation with a given id.
   348  // It returns a function to call to unregister the callback.
   349  func (c *DatabaseChanges) ForOperationID(operationID int64, cb func(*OperationStatusChange)) (CancelFunc, error) {
   350  	opIDStr := i64toa(operationID)
   351  	subscribers, err := c.getOrAddSubscribers("operations/"+opIDStr, "watch-operation", "unwatch-operation", opIDStr)
   352  	if err != nil {
   353  		return nil, err
   354  	}
   355  
   356  	filtered := func(change *OperationStatusChange) {
   357  		if change.OperationID == operationID {
   358  			cb(change)
   359  		}
   360  	}
   361  
   362  	idx := subscribers.registerOnOperationStatusChange(filtered)
   363  	cancel := func() {
   364  		subscribers.unregisterOnOperationStatusChange(idx)
   365  		c.maybeDisconnectSubscribers(subscribers)
   366  	}
   367  	return cancel, nil
   368  }
   369  
   370  // ForAllOperations registers a callback that will be called when any operation changes status.
   371  // It returns a function to call to unregister the callback.
   372  func (c *DatabaseChanges) ForAllOperations(cb func(change *OperationStatusChange)) (CancelFunc, error) {
   373  	subscribers, err := c.getOrAddSubscribers("all-operations", "watch-operations", "unwatch-operations", "")
   374  	if err != nil {
   375  		return nil, err
   376  	}
   377  
   378  	idx := subscribers.registerOnOperationStatusChange(cb)
   379  	cancel := func() {
   380  		subscribers.unregisterOnOperationStatusChange(idx)
   381  		c.maybeDisconnectSubscribers(subscribers)
   382  	}
   383  	return cancel, nil
   384  }
   385  
   386  // ForAllIndexes registers a callback that will be called when a change on any index happens.
   387  // It returns a function to call to unregister the callback.
   388  func (c *DatabaseChanges) ForAllIndexes(cb func(*IndexChange)) (CancelFunc, error) {
   389  	subscribers, err := c.getOrAddSubscribers("all-indexes", "watch-indexes", "unwatch-indexes", "")
   390  	if err != nil {
   391  		return nil, err
   392  	}
   393  
   394  	idx := subscribers.registerOnIndexChange(cb)
   395  	cancel := func() {
   396  		subscribers.unregisterOnIndexChange(idx)
   397  		c.maybeDisconnectSubscribers(subscribers)
   398  	}
   399  
   400  	return cancel, nil
   401  }
   402  
   403  // ForDocumentsStartingWith registers a callback that will be called for changes on documents whose id starts with
   404  // a given prefix. It returns a function to call to unregister the callback.
   405  func (c *DatabaseChanges) ForDocumentsStartingWith(docIDPrefix string, cb func(*DocumentChange)) (CancelFunc, error) {
   406  	subscribers, err := c.getOrAddSubscribers("prefixes/"+docIDPrefix, "watch-prefix", "unwatch-prefix", docIDPrefix)
   407  	if err != nil {
   408  		return nil, err
   409  	}
   410  	filtered := func(change *DocumentChange) {
   411  		n := len(docIDPrefix)
   412  		if n > len(change.ID) {
   413  			return
   414  		}
   415  		prefix := change.ID[:n]
   416  		if strings.EqualFold(prefix, docIDPrefix) {
   417  			cb(change)
   418  		}
   419  	}
   420  
   421  	idx := subscribers.registerOnDocumentChange(filtered)
   422  	cancel := func() {
   423  		subscribers.unregisterOnDocumentChange(idx)
   424  		c.maybeDisconnectSubscribers(subscribers)
   425  	}
   426  	return cancel, nil
   427  }
   428  
   429  // ForDocumentsInCollection registers a callback that will be called on changes for documents in a given collection.
   430  // It returns a function to call to unregister the callback.
   431  func (c *DatabaseChanges) ForDocumentsInCollection(collectionName string, cb func(*DocumentChange)) (CancelFunc, error) {
   432  	if collectionName == "" {
   433  		return nil, newIllegalArgumentError("CollectionName cannot be empty")
   434  	}
   435  
   436  	subscribers, err := c.getOrAddSubscribers("collections/"+collectionName, "watch-collection", "unwatch-collection", collectionName)
   437  	if err != nil {
   438  		return nil, err
   439  	}
   440  
   441  	filtered := func(change *DocumentChange) {
   442  		if strings.EqualFold(collectionName, change.CollectionName) {
   443  			cb(change)
   444  		}
   445  	}
   446  
   447  	idx := subscribers.registerOnDocumentChange(filtered)
   448  	cancel := func() {
   449  		subscribers.unregisterOnDocumentChange(idx)
   450  		c.maybeDisconnectSubscribers(subscribers)
   451  	}
   452  	return cancel, nil
   453  }
   454  
   455  // ForDocumentsInCollectionOfType registers a callback that will be called on changes for documents of a given type.
   456  // It returns a function to call to unregister the callback.
   457  func (c *DatabaseChanges) ForDocumentsInCollectionOfType(clazz reflect.Type, cb func(*DocumentChange)) (CancelFunc, error) {
   458  	collectionName := c.conventions.getCollectionName(clazz)
   459  	return c.ForDocumentsInCollection(collectionName, cb)
   460  }
   461  
   462  func (c *DatabaseChanges) invokeConnectionStatusChanged() {
   463  	// make a copy of callers so that we can call outside of a lock
   464  	c.mu.Lock()
   465  	dup := append([]func(){}, c.connectionStatusChanged...)
   466  	c.mu.Unlock()
   467  
   468  	for _, fn := range dup {
   469  		if fn != nil {
   470  			fn()
   471  		}
   472  	}
   473  }
   474  
   475  func (c *DatabaseChanges) AddOnError(handler func(error)) int {
   476  	c.mu.Lock()
   477  	defer c.mu.Unlock()
   478  	idx := len(c.onError)
   479  	c.onError = append(c.onError, handler)
   480  	return idx
   481  }
   482  
   483  func (c *DatabaseChanges) RemoveOnError(handlerID int) {
   484  	c.mu.Lock()
   485  	defer c.mu.Unlock()
   486  	c.onError[handlerID] = nil
   487  }
   488  
   489  // cancel outstanding commands to unblock those waiting for their completion
   490  func (c *DatabaseChanges) cancelOutstandingCommands() {
   491  	rangeFn := func(key, val interface{}) bool {
   492  		cmd := val.(*databaseChangesCommand)
   493  		cmd.confirm(true)
   494  		dcdbg("DatabaseChanges: cancelled outstanding command %d '%s %s'\n", cmd.id, cmd.command, cmd.value)
   495  		c.outstandingCommands.Delete(key)
   496  		return true
   497  	}
   498  	c.outstandingCommands.Range(rangeFn)
   499  }
   500  
   501  // Close closes DatabaseChanges and release its resources
   502  func (c *DatabaseChanges) Close() {
   503  	dcdbg("DatabaseChanges: Close()\n")
   504  	//debug.PrintStack()
   505  	select {
   506  	case <-c.chWorkCompleted:
   507  		dcdbg("DatabaseChanges.Close(): has already been closed because chanWorkCompleted notified\n")
   508  	default:
   509  		// no-op
   510  	}
   511  
   512  	c.doWorkCancel()
   513  	c.cancelOutstandingCommands()
   514  
   515  	select {
   516  	case <-c.chWorkCompleted:
   517  	case <-time.After(time.Second * 5):
   518  		dcdbg("DatabaseChanges.Close(): timed out waiting for chanWorkCompleted\n")
   519  	}
   520  
   521  	if c.onClose != nil {
   522  		c.onClose()
   523  	}
   524  }
   525  
   526  func fmtDCCommand(cmd, value string) string {
   527  	if value == "" {
   528  		return cmd
   529  	}
   530  	return cmd + " " + value
   531  }
   532  
   533  func (c *DatabaseChanges) getOrAddSubscribers(name string, watchCommand string, unwatchCommand string, value string) (*changeSubscribers, error) {
   534  	subscribersI, ok := c.subscribers.Load(name)
   535  
   536  	if ok {
   537  		return subscribersI.(*changeSubscribers), nil
   538  	}
   539  
   540  	subscribers := &changeSubscribers{
   541  		name:           name,
   542  		watchCommand:   watchCommand,
   543  		unwatchCommand: unwatchCommand,
   544  		commandValue:   value,
   545  	}
   546  	c.subscribers.Store(name, subscribers)
   547  	if err := c.connectSubscribers(subscribers); err != nil {
   548  		return nil, err
   549  	}
   550  	return subscribers, nil
   551  }
   552  
   553  func (c *DatabaseChanges) maybeDisconnectSubscribers(subscribers *changeSubscribers) {
   554  	if !subscribers.hasRegisteredHandlers() {
   555  		c.disconnectSubscribers(subscribers)
   556  	}
   557  }
   558  
   559  func (c *DatabaseChanges) disconnectSubscribers(subscribers *changeSubscribers) {
   560  	_ = c.send(subscribers.unwatchCommand, subscribers.commandValue, false)
   561  	// ignoring error: if we are not connected then we unsubscribed
   562  	// already because connections drops with all subscriptions
   563  	c.subscribers.Delete(subscribers.name)
   564  }
   565  
   566  func (c *DatabaseChanges) connectSubscribers(subscribers *changeSubscribers) error {
   567  	return c.send(subscribers.watchCommand, subscribers.commandValue, true)
   568  }
   569  
   570  func (c *DatabaseChanges) send(command, value string, waitForConfirmation bool) error {
   571  	if c.isClosed() {
   572  		return errors.New("send() called after Close()")
   573  	}
   574  
   575  	id := c.nextCommandID()
   576  	cmd := newDatabaseChangesCommand(id, command, value)
   577  	dcdbg("DatabaseChanges: send(): command id: %d, command: '%s', wait: %v\n", id, fmtDCCommand(command, value), waitForConfirmation)
   578  	if waitForConfirmation {
   579  		c.outstandingCommands.Store(id, cmd)
   580  	}
   581  
   582  	c.mu.Lock()
   583  	chCommands := c.chCommands
   584  	c.mu.Unlock()
   585  	chCommands <- cmd
   586  
   587  	if waitForConfirmation {
   588  		cmd.waitForConfirmation(time.Second * 15)
   589  	}
   590  	return nil
   591  }
   592  
   593  func startSendWorker(conn *websocket.Conn, chCommands chan *databaseChangesCommand) chan error {
   594  	chFailed := make(chan error, 1)
   595  	go func() {
   596  		dcdbg("starting a chCommands reading loop\n")
   597  		for cmd := range chCommands {
   598  			dcdbg("got command with id %d to send. Command: %s, param: %s\n", cmd.id, cmd.command, cmd.value)
   599  			o := struct {
   600  				CommandID int    `json:"CommandId"`
   601  				Command   string `json:"Command"`
   602  				Param     string `json:"Param"`
   603  			}{
   604  				CommandID: cmd.id,
   605  				Command:   cmd.command,
   606  				Param:     cmd.value,
   607  			}
   608  			err := conn.SetWriteDeadline(time.Now().Add(time.Second * 3))
   609  			if err != nil {
   610  				dcdbg("DatabaseChanges: SetWriteDeadline() failed with %s\n", err)
   611  				chFailed <- err
   612  				return
   613  			}
   614  			err = conn.WriteJSON(o)
   615  			if err != nil {
   616  				dcdbg("DatabaseChanges: conn.WriteJSON() failed with %s\n", err)
   617  				chFailed <- err
   618  				return
   619  			}
   620  			dcdbg("wrote command with id %d to socket\n", cmd.id)
   621  		}
   622  		dcdbg("DatabaseChanges: send worker finished\n")
   623  	}()
   624  	return chFailed
   625  }
   626  
   627  func toWebSocketPath(path string) string {
   628  	path = strings.Replace(path, "http://", "ws://", -1)
   629  	return strings.Replace(path, "https://", "wss://", -1)
   630  }
   631  
   632  // returns true if we should try to reconnect
   633  func (c *DatabaseChanges) doWorkInner(ctx context.Context) (error, bool) {
   634  	var err error
   635  	dialer := *websocket.DefaultDialer
   636  	dialer.HandshakeTimeout = time.Second * 2
   637  
   638  	re := c.requestExecutor
   639  	if re.Certificate != nil || re.TrustStore != nil {
   640  		dialer.TLSClientConfig, err = newTLSConfig(re.Certificate, re.TrustStore)
   641  		if err != nil {
   642  			return err, false
   643  		}
   644  	}
   645  
   646  	urlString, err := c.requestExecutor.GetURL()
   647  	if err != nil {
   648  		return err, false
   649  	}
   650  	urlString += "/databases/" + c.database + "/changes"
   651  	urlString = toWebSocketPath(urlString)
   652  
   653  	ctxDial, cancel := context.WithTimeout(ctx, time.Second*2)
   654  	var client *websocket.Conn
   655  	client, _, err = dialer.DialContext(ctxDial, urlString, nil)
   656  	cancel()
   657  
   658  	if err != nil {
   659  		dcdbg("DatabaseChanges: dialer.DialContext failed with '%s'\n", err)
   660  		return err, false
   661  	}
   662  
   663  	var chWriterFailed chan error
   664  	chWriterFailed = startSendWorker(client, c.chCommands)
   665  	var chReaderFailed chan error
   666  	chReaderFailed = c.startProcessMessagesWorker(ctx, client)
   667  
   668  	connectFn := func(key, value interface{}) bool {
   669  		subscribers := value.(*changeSubscribers)
   670  		_ = c.connectSubscribers(subscribers)
   671  		return true
   672  	}
   673  	c.subscribers.Range(connectFn)
   674  
   675  	c.invokeConnectionStatusChanged()
   676  
   677  	c.chIsConnected <- nil
   678  	// close so that subsequent channel reads also return immediately
   679  	close(c.chIsConnected)
   680  
   681  	shouldReconnect := true
   682  	err = nil
   683  	select {
   684  	case err = <-chWriterFailed:
   685  		dcdbg("DatabaseChanges: writer failed with '%s'\n", err)
   686  	case err = <-chReaderFailed:
   687  		if err != nil {
   688  			dcdbg("DatabaseChanges: reader failed with '%s'\n", err)
   689  		} else {
   690  			dcdbg("DatabaseChanges: reader finished cleanly\n")
   691  		}
   692  	case <-ctx.Done():
   693  		dcdbg("cancellation requested\n")
   694  		shouldReconnect = false
   695  	}
   696  
   697  	c.mu.Lock()
   698  	chCommands := c.chCommands
   699  	c.chCommands = make(chan *databaseChangesCommand, 32)
   700  	c.mu.Unlock()
   701  	close(chCommands)
   702  	_ = client.Close()
   703  
   704  	c.invokeConnectionStatusChanged()
   705  	return err, shouldReconnect
   706  }
   707  
   708  func (c *DatabaseChanges) doWork(ctx context.Context) error {
   709  	for {
   710  		err, shouldReconnect := c.doWorkInner(ctx)
   711  		if err != nil {
   712  			dcdbg("DatabaseChanges: doWorkInner() failed with '%s'\n", err)
   713  		}
   714  		c.cancelOutstandingCommands()
   715  		if !shouldReconnect {
   716  			return err
   717  		}
   718  		// wait before next retry
   719  		time.Sleep(time.Second)
   720  	}
   721  }
   722  
   723  func (c *DatabaseChanges) notifySubscribers(typ string, value interface{}) error {
   724  	dcdbg("DatabnaseChanges: notifySubscribers(): %s, %v\n", typ, value)
   725  	switch typ {
   726  	case "DocumentChange":
   727  		var documentChange *DocumentChange
   728  		err := decodeJSONAsStruct(value, &documentChange)
   729  		if err != nil {
   730  			dcdbg("notifySubscribers: '%s' decodeJSONAsStruct failed with %s\n", typ, err)
   731  			return err
   732  		}
   733  		fn := func(key, value interface{}) bool {
   734  			s := value.(*changeSubscribers)
   735  			s.sendDocumentChange(documentChange)
   736  			return true
   737  		}
   738  		c.subscribers.Range(fn)
   739  	case "IndexChange":
   740  		var indexChange *IndexChange
   741  		err := decodeJSONAsStruct(value, &indexChange)
   742  		if err != nil {
   743  			dcdbg("notifySubscribers: '%s' decodeJSONAsStruct failed with %s\n", typ, err)
   744  			return err
   745  		}
   746  		fn := func(key, value interface{}) bool {
   747  			s := value.(*changeSubscribers)
   748  			s.sendIndexChange(indexChange)
   749  			return true
   750  		}
   751  		c.subscribers.Range(fn)
   752  	case "OperationStatusChange":
   753  		var operationStatusChange *OperationStatusChange
   754  		err := decodeJSONAsStruct(value, &operationStatusChange)
   755  		if err != nil {
   756  			dcdbg("notifySubscribers: '%s' decodeJSONAsStruct failed with %s\n", typ, err)
   757  			return err
   758  		}
   759  		fn := func(key, value interface{}) bool {
   760  			s := value.(*changeSubscribers)
   761  			s.sendOperationStatusChange(operationStatusChange)
   762  			return true
   763  		}
   764  		c.subscribers.Range(fn)
   765  	default:
   766  		dcdbg("DatabnaseChanges: notifySubscribers(): unsupported type '%s'\n", typ)
   767  		return fmt.Errorf("notifySubscribers: unsupported type '%s'", typ)
   768  	}
   769  	return nil
   770  }
   771  
   772  func (c *DatabaseChanges) notifyAboutError(err error) {
   773  	if c.isClosed() {
   774  		return
   775  	}
   776  	panicIf(err == nil, "err is nil")
   777  	c.lastError.Store(err)
   778  
   779  	// make a copy so that we can call outside of a lock
   780  	c.mu.Lock()
   781  	handlers := append([]func(error){}, c.onError...)
   782  	c.mu.Unlock()
   783  
   784  	for _, fn := range handlers {
   785  		if fn != nil {
   786  			fn(err)
   787  		}
   788  	}
   789  }
   790  
   791  func (c *DatabaseChanges) startProcessMessagesWorker(ctx context.Context, conn *websocket.Conn) chan error {
   792  	chFailed := make(chan error, 1)
   793  	go func() {
   794  		var err error
   795  		for {
   796  			var msgArray []interface{} // an array of objects
   797  			err = conn.ReadJSON(&msgArray)
   798  			if err != nil {
   799  				if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
   800  					dcdbg("DatabaseChanges: ReadJSON() failed with %s\n", err)
   801  				} else {
   802  					dcdbg("DatabaseChanges: ReadJSON() failed with %s, turning into no error\n", err)
   803  					err = nil
   804  				}
   805  				break
   806  			}
   807  			if len(msgArray) == 0 {
   808  				continue
   809  			}
   810  
   811  			if enableDatabaseChangesDebugOutput {
   812  				s, _ := json.Marshal(msgArray)
   813  				dcdbg("DatatabaseChange: received messages:\n%s\n", s)
   814  			}
   815  
   816  			for _, msgNodeV := range msgArray {
   817  				msgNode := msgNodeV.(map[string]interface{})
   818  				typ, ok := jsonGetAsText(msgNode, "Type")
   819  				if !ok {
   820  					continue
   821  				}
   822  				switch typ {
   823  				case "Error":
   824  					errStr, _ := jsonGetAsText(msgNode, "Error")
   825  					c.notifyAboutError(newRuntimeError("%s", errStr))
   826  				case "Confirm":
   827  					commandID, ok := jsonGetAsInt(msgNode, "CommandId")
   828  					if ok {
   829  						v, ok := c.outstandingCommands.Load(commandID)
   830  						if ok {
   831  							cmd := v.(*databaseChangesCommand)
   832  							cmd.confirm(false)
   833  							dcdbg("DatabaseChanges: confirmed command id %d, command '%s'\n", cmd.id, fmtDCCommand(cmd.command, cmd.value))
   834  						}
   835  					}
   836  				default:
   837  					if val, ok := msgNode["Value"]; ok {
   838  						// sometimes a message is {"TopologyChange":true}
   839  						_ = c.notifySubscribers(typ, val)
   840  					}
   841  				}
   842  			}
   843  		}
   844  		if err != nil {
   845  			dcdbg("Not cancelled so calling notifyAboutError(), err = %v\n", err)
   846  			c.notifyAboutError(err)
   847  		}
   848  		chFailed <- err
   849  	}()
   850  	return chFailed
   851  }