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

     1  package ravendb
     2  
     3  import (
     4  	"crypto/tls"
     5  	"crypto/x509"
     6  	"strings"
     7  	"sync"
     8  	"time"
     9  )
    10  
    11  // Note: Java's IDocumentStore is DocumentStore
    12  // Note: Java's DocumentStoreBase is folded into DocumentStore
    13  
    14  // DocumentStore represents a database
    15  type DocumentStore struct {
    16  	// from DocumentStoreBase
    17  	onBeforeStore      []func(*BeforeStoreEventArgs)
    18  	onAfterSaveChanges []func(*AfterSaveChangesEventArgs)
    19  
    20  	onBeforeDelete []func(*BeforeDeleteEventArgs)
    21  	onBeforeQuery  []func(*BeforeQueryEventArgs)
    22  	// TODO: there's no way to register for this event
    23  	onSessionCreated []func(*SessionCreatedEventArgs)
    24  	subscriptions    *DocumentSubscriptions
    25  
    26  	disposed    bool
    27  	conventions *DocumentConventions
    28  	urls        []string // urls for HTTP endopoints of server nodes
    29  	initialized bool
    30  	Certificate *tls.Certificate
    31  	TrustStore  *x509.Certificate
    32  	database    string // name of the database
    33  
    34  	// maps database name to DatabaseChanges. Must be protected with mutex
    35  	databaseChanges map[string]*DatabaseChanges
    36  
    37  	// Note: access must be protected with mu
    38  	// Lazy.Value is **EvictItemsFromCacheBasedOnChanges
    39  	aggressiveCacheChanges map[string]*evictItemsFromCacheBasedOnChanges
    40  
    41  	// maps database name to its RequestsExecutor
    42  	// access must be protected with mu
    43  	// TODO: in Java is ConcurrentMap<String, RequestExecutor> requestExecutors
    44  	// so must protect access with mutex and use case-insensitive lookup
    45  	requestsExecutors map[string]*RequestExecutor
    46  
    47  	multiDbHiLo                  *MultiDatabaseHiLoIDGenerator
    48  	maintenanceOperationExecutor *MaintenanceOperationExecutor
    49  	operationExecutor            *OperationExecutor
    50  	identifier                   string
    51  	aggressiveCachingUsed        bool
    52  
    53  	afterClose  []func(*DocumentStore)
    54  	beforeClose []func(*DocumentStore)
    55  
    56  	mu sync.Mutex
    57  }
    58  
    59  // methods from DocumentStoreBase
    60  
    61  // GetConventions returns DocumentConventions
    62  func (s *DocumentStore) GetConventions() *DocumentConventions {
    63  	if s.conventions == nil {
    64  		s.conventions = NewDocumentConventions()
    65  	}
    66  	return s.conventions
    67  }
    68  
    69  // SetConventions sets DocumentConventions
    70  func (s *DocumentStore) SetConventions(conventions *DocumentConventions) {
    71  	s.assertNotInitialized("conventions")
    72  	s.conventions = conventions
    73  }
    74  
    75  // Subscriptions returns DocumentSubscriptions which allows subscribing to changes in store
    76  func (s *DocumentStore) Subscriptions() *DocumentSubscriptions {
    77  	return s.subscriptions
    78  }
    79  
    80  // GetUrls returns urls of all RavenDB nodes
    81  func (s *DocumentStore) GetUrls() []string {
    82  	return s.urls
    83  }
    84  
    85  // SetUrls sets initial urls of RavenDB nodes
    86  func (s *DocumentStore) SetUrls(urls []string) {
    87  	panicIf(len(urls) == 0, "urls is empty")
    88  	s.assertNotInitialized("urls")
    89  	for i, s := range urls {
    90  		urls[i] = strings.TrimSuffix(s, "/")
    91  	}
    92  	s.urls = urls
    93  }
    94  
    95  func (s *DocumentStore) ensureNotClosed() error {
    96  	if s.disposed {
    97  		return newIllegalStateError("The document store has already been disposed and cannot be used")
    98  	}
    99  	return nil
   100  }
   101  
   102  // AddBeforeStoreStoreListener registers a function that will be called before storing ab entity.
   103  // It'll be registered with every new session.
   104  // Returns listener id that can be passed to RemoveBeforeStoreListener to unregister
   105  // the listener.
   106  func (s *DocumentStore) AddBeforeStoreListener(handler func(*BeforeStoreEventArgs)) int {
   107  	id := len(s.onBeforeStore)
   108  	s.onBeforeStore = append(s.onBeforeStore, handler)
   109  	return id
   110  
   111  }
   112  
   113  // RemoveBeforeStoreListener removes a listener given id returned by AddBeforeStoreListener
   114  func (s *DocumentStore) RemoveBeforeStoreListener(handlerID int) {
   115  	s.onBeforeStore[handlerID] = nil
   116  }
   117  
   118  // AddAfterSaveChangesListener registers a function that will be called before saving changes.
   119  // It'll be registered with every new session.
   120  // Returns listener id that can be passed to RemoveAfterSaveChangesListener to unregister
   121  // the listener.
   122  func (s *DocumentStore) AddAfterSaveChangesListener(handler func(*AfterSaveChangesEventArgs)) int {
   123  	s.onAfterSaveChanges = append(s.onAfterSaveChanges, handler)
   124  	return len(s.onAfterSaveChanges) - 1
   125  }
   126  
   127  // RemoveAfterSaveChangesListener removes a listener given id returned by AddAfterSaveChangesListener
   128  func (s *DocumentStore) RemoveAfterSaveChangesListener(handlerID int) {
   129  	s.onAfterSaveChanges[handlerID] = nil
   130  }
   131  
   132  // AddBeforeDeleteListener registers a function that will be called before deleting an entity.
   133  // It'll be registered with every new session.
   134  // Returns listener id that can be passed to RemoveBeforeDeleteListener to unregister
   135  // the listener.
   136  func (s *DocumentStore) AddBeforeDeleteListener(handler func(*BeforeDeleteEventArgs)) int {
   137  	s.onBeforeDelete = append(s.onBeforeDelete, handler)
   138  	return len(s.onBeforeDelete) - 1
   139  }
   140  
   141  // RemoveBeforeDeleteListener removes a listener given id returned by AddBeforeDeleteListener
   142  func (s *DocumentStore) RemoveBeforeDeleteListener(handlerID int) {
   143  	s.onBeforeDelete[handlerID] = nil
   144  }
   145  
   146  // AddBeforeQueryListener registers a function that will be called before running a query.
   147  // It allows customizing query via DocumentQueryCustomization.
   148  // It'll be registered with every new session.
   149  // Returns listener id that can be passed to RemoveBeforeQueryListener to unregister
   150  // the listener.
   151  func (s *DocumentStore) AddBeforeQueryListener(handler func(*BeforeQueryEventArgs)) int {
   152  	s.onBeforeQuery = append(s.onBeforeQuery, handler)
   153  	return len(s.onBeforeQuery) - 1
   154  }
   155  
   156  // RemoveBeforeQueryListener removes a listener given id returned by AddBeforeQueryListener
   157  func (s *DocumentStore) RemoveBeforeQueryListener(handlerID int) {
   158  	s.onBeforeQuery[handlerID] = nil
   159  }
   160  
   161  func (s *DocumentStore) registerEvents(session *InMemoryDocumentSessionOperations) {
   162  	// TODO: unregister those events?
   163  	for _, handler := range s.onBeforeStore {
   164  		if handler != nil {
   165  			session.AddBeforeStoreListener(handler)
   166  		}
   167  	}
   168  
   169  	for _, handler := range s.onAfterSaveChanges {
   170  		if handler != nil {
   171  			session.AddAfterSaveChangesListener(handler)
   172  		}
   173  	}
   174  
   175  	for _, handler := range s.onBeforeDelete {
   176  		if handler != nil {
   177  			session.AddBeforeDeleteListener(handler)
   178  		}
   179  	}
   180  
   181  	for _, handler := range s.onBeforeQuery {
   182  		if handler != nil {
   183  			session.AddBeforeQueryListener(handler)
   184  		}
   185  	}
   186  }
   187  
   188  func (s *DocumentStore) afterSessionCreated(session *InMemoryDocumentSessionOperations) {
   189  	for _, handler := range s.onSessionCreated {
   190  		if handler != nil {
   191  			args := &SessionCreatedEventArgs{
   192  				Session: session,
   193  			}
   194  			handler(args)
   195  		}
   196  	}
   197  }
   198  
   199  func (s *DocumentStore) assertInitialized() error {
   200  	if !s.initialized {
   201  		return newIllegalStateError("DocumentStore must be initialized")
   202  	}
   203  	return nil
   204  }
   205  
   206  func (s *DocumentStore) assertNotInitialized(property string) {
   207  	panicIf(s.initialized, "You cannot set '%s' after the document store has been initialized.", property)
   208  }
   209  
   210  func (s *DocumentStore) GetDatabase() string {
   211  	return s.database
   212  }
   213  
   214  func (s *DocumentStore) SetDatabase(database string) {
   215  	s.assertNotInitialized("database")
   216  	s.database = database
   217  }
   218  
   219  func (s *DocumentStore) AggressivelyCache(database string) (CancelFunc, error) {
   220  	return s.AggressivelyCacheForDatabase(time.Hour*24, database)
   221  }
   222  
   223  func newDocumentStore() *DocumentStore {
   224  	s := &DocumentStore{
   225  		requestsExecutors:      map[string]*RequestExecutor{},
   226  		conventions:            NewDocumentConventions(),
   227  		databaseChanges:        map[string]*DatabaseChanges{},
   228  		aggressiveCacheChanges: map[string]*evictItemsFromCacheBasedOnChanges{},
   229  	}
   230  	s.subscriptions = newDocumentSubscriptions(s)
   231  	return s
   232  }
   233  
   234  func NewDocumentStore(urls []string, database string) *DocumentStore {
   235  	res := newDocumentStore()
   236  	if len(urls) > 0 {
   237  		res.SetUrls(urls)
   238  	}
   239  	if database != "" {
   240  		res.SetDatabase(database)
   241  	}
   242  	return res
   243  }
   244  
   245  // Get an identifier of the store. For debugging / testing.
   246  func (s *DocumentStore) GetIdentifier() string {
   247  	if s.identifier != "" {
   248  		return s.identifier
   249  	}
   250  
   251  	if len(s.urls) == 0 {
   252  		return ""
   253  	}
   254  
   255  	if s.database != "" {
   256  		return strings.Join(s.urls, ",") + " (DB: " + s.database + ")"
   257  	}
   258  
   259  	return strings.Join(s.urls, ",")
   260  }
   261  
   262  func (s *DocumentStore) SetIdentifier(identifier string) {
   263  	s.identifier = identifier
   264  }
   265  
   266  // Close closes the Store
   267  func (s *DocumentStore) Close() {
   268  	if s.disposed {
   269  		redbg("DocumentStore.Close: already disposed\n")
   270  		return
   271  	}
   272  	redbg("DocumentStore.Close\n")
   273  
   274  	for _, fn := range s.beforeClose {
   275  		fn(s)
   276  	}
   277  	s.beforeClose = nil
   278  
   279  	for _, evict := range s.aggressiveCacheChanges {
   280  		evict.Close()
   281  	}
   282  
   283  	for _, changes := range s.databaseChanges {
   284  		changes.Close()
   285  	}
   286  
   287  	if s.multiDbHiLo != nil {
   288  		s.multiDbHiLo.ReturnUnusedRange()
   289  	}
   290  
   291  	if s.Subscriptions() != nil {
   292  		_ = s.Subscriptions().Close()
   293  	}
   294  
   295  	s.disposed = true
   296  
   297  	for _, fn := range s.afterClose {
   298  		fn(s)
   299  	}
   300  	s.afterClose = nil
   301  
   302  	for _, re := range s.requestsExecutors {
   303  		re.Close()
   304  	}
   305  }
   306  
   307  // OpenSession opens a new session to document Store.
   308  // If database is not given, we'll use store's database name
   309  func (s *DocumentStore) OpenSession(database string) (*DocumentSession, error) {
   310  	sessionOptions := &SessionOptions{
   311  		Database: database,
   312  	}
   313  	return s.OpenSessionWithOptions(sessionOptions)
   314  }
   315  
   316  func (s *DocumentStore) OpenSessionWithOptions(options *SessionOptions) (*DocumentSession, error) {
   317  	if err := s.assertInitialized(); err != nil {
   318  		return nil, err
   319  	}
   320  	if err := s.ensureNotClosed(); err != nil {
   321  		return nil, err
   322  	}
   323  
   324  	sessionID := NewUUID().String()
   325  	databaseName := options.Database
   326  	if databaseName == "" {
   327  		databaseName = s.GetDatabase()
   328  	}
   329  	requestExecutor := options.RequestExecutor
   330  	if requestExecutor == nil {
   331  		requestExecutor = s.GetRequestExecutor(databaseName)
   332  	}
   333  	session := NewDocumentSession(databaseName, s, sessionID, requestExecutor)
   334  	s.registerEvents(session.InMemoryDocumentSessionOperations)
   335  	s.afterSessionCreated(session.InMemoryDocumentSessionOperations)
   336  	return session, nil
   337  }
   338  
   339  func (s *DocumentStore) ExecuteIndex(task *IndexCreationTask, database string) error {
   340  	if err := s.assertInitialized(); err != nil {
   341  		return err
   342  	}
   343  	return task.Execute(s, s.conventions, database)
   344  }
   345  
   346  func (s *DocumentStore) ExecuteIndexes(tasks []*IndexCreationTask, database string) error {
   347  	if err := s.assertInitialized(); err != nil {
   348  		return err
   349  	}
   350  	indexesToAdd := indexCreationCreateIndexesToAdd(tasks, s.conventions)
   351  
   352  	op := NewPutIndexesOperation(indexesToAdd...)
   353  	if database == "" {
   354  		database = s.GetDatabase()
   355  	}
   356  	return s.Maintenance().ForDatabase(database).Send(op)
   357  }
   358  
   359  // GetRequestExecutor gets a request executor.
   360  // database is optional
   361  func (s *DocumentStore) GetRequestExecutor(database string) *RequestExecutor {
   362  	must(s.assertInitialized())
   363  	if database == "" {
   364  		database = s.GetDatabase()
   365  	}
   366  	database = strings.ToLower(database)
   367  
   368  	s.mu.Lock()
   369  	executor, ok := s.requestsExecutors[database]
   370  	s.mu.Unlock()
   371  
   372  	if ok {
   373  		return executor
   374  	}
   375  
   376  	if !s.GetConventions().IsDisableTopologyUpdates() {
   377  		executor = RequestExecutorCreate(s.GetUrls(), database, s.Certificate, s.TrustStore, s.GetConventions())
   378  	} else {
   379  		executor = RequestExecutorCreateForSingleNodeWithConfigurationUpdates(s.GetUrls()[0], database, s.Certificate, s.TrustStore, s.GetConventions())
   380  	}
   381  
   382  	s.mu.Lock()
   383  	s.requestsExecutors[database] = executor
   384  	s.mu.Unlock()
   385  
   386  	return executor
   387  }
   388  
   389  // Initialize initializes document Store,
   390  // Must be called before executing any operation.
   391  func (s *DocumentStore) Initialize() error {
   392  	if s.initialized {
   393  		return nil
   394  	}
   395  	err := s.assertValidConfiguration()
   396  	if err != nil {
   397  		return err
   398  	}
   399  
   400  	conventions := s.conventions
   401  	if conventions.GetDocumentIDGenerator() == nil {
   402  		generator := NewMultiDatabaseHiLoIDGenerator(s, s.GetConventions())
   403  		s.multiDbHiLo = generator
   404  		genID := func(dbName string, entity interface{}) (string, error) {
   405  			return generator.GenerateDocumentID(dbName, entity)
   406  		}
   407  		conventions.SetDocumentIDGenerator(genID)
   408  	}
   409  	s.initialized = true
   410  	return nil
   411  }
   412  
   413  func (s *DocumentStore) assertValidConfiguration() error {
   414  	if len(s.urls) == 0 {
   415  		return newIllegalArgumentError("Must provide urls to NewDocumentStore")
   416  	}
   417  	return nil
   418  }
   419  
   420  type RestoreCaching struct {
   421  	re  *RequestExecutor
   422  	old *AggressiveCacheOptions
   423  }
   424  
   425  func (r *RestoreCaching) Close() error {
   426  	r.re.aggressiveCaching = r.old
   427  	return nil
   428  }
   429  
   430  func (s *DocumentStore) DisableAggressiveCaching(databaseName string) *RestoreCaching {
   431  	if databaseName == "" {
   432  		databaseName = s.GetDatabase()
   433  	}
   434  
   435  	re := s.GetRequestExecutor(databaseName)
   436  	old := re.aggressiveCaching
   437  	re.aggressiveCaching = nil
   438  	res := &RestoreCaching{
   439  		re:  re,
   440  		old: old,
   441  	}
   442  	return res
   443  }
   444  
   445  func (s *DocumentStore) Changes(database string) *DatabaseChanges {
   446  	must(s.assertInitialized())
   447  
   448  	if database == "" {
   449  		database = s.GetDatabase()
   450  	}
   451  
   452  	s.mu.Lock()
   453  	changes, ok := s.databaseChanges[database]
   454  	s.mu.Unlock()
   455  
   456  	if !ok {
   457  		changes = s.createDatabaseChanges(database)
   458  
   459  		s.mu.Lock()
   460  		s.databaseChanges[database] = changes
   461  		s.mu.Unlock()
   462  
   463  	}
   464  	return changes
   465  }
   466  
   467  func (s *DocumentStore) createDatabaseChanges(database string) *DatabaseChanges {
   468  	panicIf(database == "", "database can't be empty string")
   469  	onDispose := func() {
   470  		s.mu.Lock()
   471  		delete(s.databaseChanges, database)
   472  		s.mu.Unlock()
   473  	}
   474  	re := s.GetRequestExecutor(database)
   475  	return newDatabaseChanges(re, database, onDispose)
   476  }
   477  
   478  func (s *DocumentStore) GetLastDatabaseChangesStateError(database string) error {
   479  	if database == "" {
   480  		database = s.GetDatabase()
   481  	}
   482  
   483  	s.mu.Lock()
   484  	databaseChanges, ok := s.databaseChanges[database]
   485  	s.mu.Unlock()
   486  
   487  	if !ok {
   488  		return nil
   489  	}
   490  	ch := databaseChanges
   491  	return ch.getLastConnectionStateError()
   492  }
   493  
   494  func (s *DocumentStore) AggressivelyCacheFor(cacheDuration time.Duration) (CancelFunc, error) {
   495  	return s.AggressivelyCacheForDatabase(cacheDuration, "")
   496  }
   497  
   498  func (s *DocumentStore) AggressivelyCacheForDatabase(cacheDuration time.Duration, database string) (CancelFunc, error) {
   499  	if database == "" {
   500  		database = s.GetDatabase()
   501  	}
   502  	if database == "" {
   503  		return nil, newIllegalArgumentError("must have database")
   504  	}
   505  	s.mu.Lock()
   506  	cachingUsed := s.aggressiveCachingUsed
   507  	s.mu.Unlock()
   508  	if !cachingUsed {
   509  		err := s.listenToChangesAndUpdateTheCache(database)
   510  		if err != nil {
   511  			return nil, err
   512  		}
   513  	}
   514  
   515  	// TODO: protect access to aggressiveCaching
   516  	opts := &AggressiveCacheOptions{
   517  		Duration: cacheDuration,
   518  	}
   519  	re := s.GetRequestExecutor(database)
   520  	oldOpts := re.aggressiveCaching
   521  	re.aggressiveCaching = opts
   522  
   523  	restorer := func() {
   524  		re.aggressiveCaching = oldOpts
   525  	}
   526  	return restorer, nil
   527  }
   528  
   529  func (s *DocumentStore) listenToChangesAndUpdateTheCache(database string) error {
   530  	s.mu.Lock()
   531  	s.aggressiveCachingUsed = true
   532  	evict := s.aggressiveCacheChanges[database]
   533  	s.mu.Unlock()
   534  
   535  	if evict != nil {
   536  		return nil
   537  	}
   538  
   539  	evict, err := newEvictItemsFromCacheBasedOnChanges(s, database)
   540  	if err != nil {
   541  		return err
   542  	}
   543  	s.mu.Lock()
   544  	s.aggressiveCacheChanges[database] = evict
   545  	s.mu.Unlock()
   546  	return nil
   547  }
   548  
   549  func (s *DocumentStore) AddBeforeCloseListener(fn func(*DocumentStore)) int {
   550  	s.beforeClose = append(s.beforeClose, fn)
   551  	return len(s.beforeClose) - 1
   552  }
   553  
   554  func (s *DocumentStore) RemoveBeforeCloseListener(idx int) {
   555  	s.beforeClose[idx] = nil
   556  }
   557  
   558  func (s *DocumentStore) AddAfterCloseListener(fn func(*DocumentStore)) int {
   559  	s.afterClose = append(s.afterClose, fn)
   560  	return len(s.afterClose) - 1
   561  }
   562  
   563  func (s *DocumentStore) RemoveAfterCloseListener(idx int) {
   564  	s.afterClose[idx] = nil
   565  }
   566  
   567  func (s *DocumentStore) Maintenance() *MaintenanceOperationExecutor {
   568  	must(s.assertInitialized())
   569  
   570  	if s.maintenanceOperationExecutor == nil {
   571  		s.maintenanceOperationExecutor = NewMaintenanceOperationExecutor(s, "")
   572  	}
   573  
   574  	return s.maintenanceOperationExecutor
   575  }
   576  
   577  func (s *DocumentStore) Operations() *OperationExecutor {
   578  	if s.operationExecutor == nil {
   579  		s.operationExecutor = NewOperationExecutor(s, "")
   580  	}
   581  
   582  	return s.operationExecutor
   583  }
   584  
   585  func (s *DocumentStore) BulkInsert(database string) *BulkInsertOperation {
   586  	if database == "" {
   587  		database = s.GetDatabase()
   588  	}
   589  	return NewBulkInsertOperation(database, s)
   590  }