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 }