github.com/cyverse/go-irodsclient@v0.13.2/irods/session/session.go (about)

     1  package session
     2  
     3  import (
     4  	"sync"
     5  	"time"
     6  
     7  	"github.com/cyverse/go-irodsclient/irods/connection"
     8  	"github.com/cyverse/go-irodsclient/irods/metrics"
     9  	"github.com/cyverse/go-irodsclient/irods/types"
    10  	log "github.com/sirupsen/logrus"
    11  	"golang.org/x/xerrors"
    12  )
    13  
    14  // TransactionFailureHandler is an handler that is called when transaction operation fails
    15  type TransactionFailureHandler func(commitFail bool, poormansRollbackFail bool)
    16  
    17  // IRODSSession manages connections to iRODS
    18  type IRODSSession struct {
    19  	account                   *types.IRODSAccount
    20  	config                    *IRODSSessionConfig
    21  	connectionPool            *ConnectionPool
    22  	sharedConnections         map[*connection.IRODSConnection]int
    23  	startNewTransaction       bool
    24  	commitFail                bool
    25  	poormansRollbackFail      bool
    26  	transactionFailureHandler TransactionFailureHandler
    27  
    28  	lastConnectionError     error
    29  	lastConnectionErrorTime time.Time
    30  
    31  	supportParallelUpload    bool
    32  	supportParallelUploadSet bool
    33  
    34  	metrics metrics.IRODSMetrics
    35  	mutex   sync.Mutex
    36  }
    37  
    38  // NewIRODSSession create a IRODSSession
    39  func NewIRODSSession(account *types.IRODSAccount, config *IRODSSessionConfig) (*IRODSSession, error) {
    40  	sess := IRODSSession{
    41  		account:           account,
    42  		config:            config,
    43  		sharedConnections: map[*connection.IRODSConnection]int{},
    44  
    45  		// transaction
    46  		startNewTransaction:       config.StartNewTransaction,
    47  		commitFail:                false,
    48  		poormansRollbackFail:      false,
    49  		transactionFailureHandler: nil,
    50  
    51  		lastConnectionError:     nil,
    52  		lastConnectionErrorTime: time.Time{},
    53  
    54  		supportParallelUpload:    false,
    55  		supportParallelUploadSet: false,
    56  
    57  		metrics: metrics.IRODSMetrics{},
    58  
    59  		mutex: sync.Mutex{},
    60  	}
    61  
    62  	poolConfig := ConnectionPoolConfig{
    63  		Account:          account,
    64  		ApplicationName:  config.ApplicationName,
    65  		InitialCap:       config.ConnectionInitNumber,
    66  		MaxIdle:          config.ConnectionMaxIdle,
    67  		MaxCap:           config.ConnectionMax,
    68  		Lifespan:         config.ConnectionLifespan,
    69  		IdleTimeout:      config.ConnectionIdleTimeout,
    70  		OperationTimeout: config.OperationTimeout,
    71  		TcpBufferSize:    config.TcpBufferSize,
    72  	}
    73  
    74  	pool, err := NewConnectionPool(&poolConfig, &sess.metrics)
    75  	if err != nil {
    76  		sess.lastConnectionError = err
    77  		sess.lastConnectionErrorTime = time.Now()
    78  
    79  		return nil, xerrors.Errorf("failed to create connection pool: %w", err)
    80  	}
    81  	sess.connectionPool = pool
    82  
    83  	// set transaction config
    84  	// when the user is anonymous, we cannot use transaction since we don't have access to home dir
    85  	if sess.account.ClientUser == "anonymous" {
    86  		sess.commitFail = true
    87  		sess.poormansRollbackFail = true
    88  	}
    89  
    90  	return &sess, nil
    91  }
    92  
    93  // IsConnectionError returns if there is a failure
    94  func (sess *IRODSSession) GetLastConnectionError() (time.Time, error) {
    95  	sess.mutex.Lock()
    96  	defer sess.mutex.Unlock()
    97  
    98  	return sess.lastConnectionErrorTime, sess.lastConnectionError
    99  }
   100  
   101  func (sess *IRODSSession) getPendingError() error {
   102  	if sess.lastConnectionError == nil {
   103  		return nil
   104  	}
   105  
   106  	if types.IsPermanantFailure(sess.lastConnectionError) {
   107  		return sess.lastConnectionError
   108  	}
   109  
   110  	// transitive error
   111  	// check timeout
   112  	if sess.lastConnectionErrorTime.Add(sess.config.ConnectionErrorTimeout).Before(time.Now()) {
   113  		// passed timeout
   114  		return nil
   115  	}
   116  
   117  	return sess.lastConnectionError
   118  }
   119  
   120  // IsPermanantFailure returns if there is a failure that is unfixable, permanant
   121  func (sess *IRODSSession) IsPermanantFailure() bool {
   122  	sess.mutex.Lock()
   123  	defer sess.mutex.Unlock()
   124  
   125  	return types.IsPermanantFailure(sess.lastConnectionError)
   126  }
   127  
   128  // GetConfig returns a configuration
   129  func (sess *IRODSSession) GetConfig() *IRODSSessionConfig {
   130  	return sess.config
   131  }
   132  
   133  // GetAccount returns an account
   134  func (sess *IRODSSession) GetAccount() *types.IRODSAccount {
   135  	return sess.account
   136  }
   137  
   138  // SetTransactionFailureHandler sets transaction failure handler
   139  func (sess *IRODSSession) SetTransactionFailureHandler(handler TransactionFailureHandler) {
   140  	sess.transactionFailureHandler = handler
   141  }
   142  
   143  // SetCommitFail sets commit fail
   144  func (sess *IRODSSession) SetCommitFail(commitFail bool) {
   145  	sess.commitFail = commitFail
   146  }
   147  
   148  // SetPoormansRollbackFail sets poormans rollback fail
   149  func (sess *IRODSSession) SetPoormansRollbackFail(poormansRollbackFail bool) {
   150  	sess.poormansRollbackFail = poormansRollbackFail
   151  }
   152  
   153  // endTransaction ends transaction
   154  func (sess *IRODSSession) endTransaction(conn *connection.IRODSConnection) error {
   155  	logger := log.WithFields(log.Fields{
   156  		"package":  "session",
   157  		"struct":   "IRODSSession",
   158  		"function": "endTransaction",
   159  	})
   160  
   161  	// Each irods connection automatically starts a database transaction at initial setup.
   162  	// All queries against irods using a connection will give results corresponding to the time
   163  	// the connection was made, or since the last change using the very same connection.
   164  	// I.e. if connections 1 and 2 are created at the same time, and connection 1 does an update,
   165  	// connection 2 will not see it until any other change is made using connection 2.
   166  	// The connection we get here from the connection pool might be old, and we might miss
   167  	// changes that happened in parallel connections. We fix this by doing a rollback operation,
   168  	// which will do nothing to the database (there are no operations staged for commit/rollback),
   169  	// but which will close the current transaction and starts a new one - refreshing the view for
   170  	// future queries.
   171  
   172  	if !sess.startNewTransaction {
   173  		// done
   174  		return nil
   175  	}
   176  
   177  	if !sess.commitFail {
   178  		commitErr := conn.Commit()
   179  		if commitErr == nil {
   180  			return nil
   181  		}
   182  
   183  		// failed to commit
   184  		sess.commitFail = true
   185  		logger.WithError(commitErr).Debug("failed to commit transaction")
   186  
   187  		if sess.transactionFailureHandler != nil {
   188  			sess.transactionFailureHandler(sess.commitFail, sess.poormansRollbackFail)
   189  		}
   190  	}
   191  
   192  	if !sess.poormansRollbackFail {
   193  		// try rollback
   194  		rollbackErr := conn.PoorMansRollback()
   195  		if rollbackErr == nil {
   196  			return nil
   197  		}
   198  
   199  		// failed to rollback
   200  		sess.poormansRollbackFail = true
   201  		logger.WithError(rollbackErr).Debug("failed to rollback (poorman) transaction")
   202  
   203  		if sess.transactionFailureHandler != nil {
   204  			sess.transactionFailureHandler(sess.commitFail, sess.poormansRollbackFail)
   205  		}
   206  	}
   207  
   208  	return xerrors.Errorf("failed to commit/rollback transaction")
   209  }
   210  
   211  // AcquireConnection returns an idle connection
   212  func (sess *IRODSSession) AcquireConnection() (*connection.IRODSConnection, error) {
   213  	logger := log.WithFields(log.Fields{
   214  		"package":  "session",
   215  		"struct":   "IRODSSession",
   216  		"function": "AcquireConnection",
   217  	})
   218  
   219  	sess.mutex.Lock()
   220  	defer sess.mutex.Unlock()
   221  
   222  	// return last error
   223  	pendingErr := sess.getPendingError()
   224  	if pendingErr != nil {
   225  		return nil, xerrors.Errorf("failed to get a connection from the pool because pending error is found: %w", pendingErr)
   226  	}
   227  
   228  	// check if there are available connections in the pool
   229  	if sess.connectionPool.AvailableConnections() > 0 {
   230  		// try to get it from the pool
   231  		conn, _, err := sess.connectionPool.Get()
   232  		// ignore error this happens when connections in the pool are all occupied
   233  		if err != nil {
   234  			if types.IsConnectionPoolFullError(err) {
   235  				logger.WithError(err).Debug("failed to get a connection from the pool, the pool is full")
   236  				// fall below
   237  			} else {
   238  				// fail
   239  				sess.lastConnectionError = err
   240  				sess.lastConnectionErrorTime = time.Now()
   241  
   242  				return nil, err
   243  			}
   244  		} else {
   245  			// put to share
   246  			if shares, ok := sess.sharedConnections[conn]; ok {
   247  				shares++
   248  				sess.sharedConnections[conn] = shares
   249  			} else {
   250  				sess.sharedConnections[conn] = 1
   251  			}
   252  
   253  			if !sess.supportParallelUploadSet {
   254  				sess.supportParallelUpload = conn.SupportParallelUpload()
   255  				sess.supportParallelUploadSet = true
   256  			}
   257  
   258  			return conn, nil
   259  		}
   260  	}
   261  
   262  	// failed to get connection from pool
   263  	// find a connection from shared connection list that has minimum share count
   264  	logger.Debug("Share an in-use connection as it cannot create a new connection")
   265  	minShare := 0
   266  	var minShareConn *connection.IRODSConnection
   267  	for sharedConn, shareCount := range sess.sharedConnections {
   268  		if minShare == 0 || shareCount < minShare {
   269  			minShare = shareCount
   270  			minShareConn = sharedConn
   271  		}
   272  
   273  		if minShare == 1 {
   274  			// can't be smaller
   275  			break
   276  		}
   277  	}
   278  
   279  	if minShareConn == nil {
   280  		sess.metrics.IncreaseCounterForConnectionPoolFailures(1)
   281  		return nil, xerrors.Errorf("failed to get a shared connection, too many connections created")
   282  	}
   283  
   284  	// update
   285  	minShare++
   286  	sess.sharedConnections[minShareConn] = minShare
   287  
   288  	return minShareConn, nil
   289  }
   290  
   291  // AcquireConnectionsMulti returns idle connections
   292  func (sess *IRODSSession) AcquireConnectionsMulti(number int) ([]*connection.IRODSConnection, error) {
   293  	logger := log.WithFields(log.Fields{
   294  		"package":  "session",
   295  		"struct":   "IRODSSession",
   296  		"function": "AcquireConnectionsMulti",
   297  	})
   298  
   299  	sess.mutex.Lock()
   300  	defer sess.mutex.Unlock()
   301  
   302  	// return last error
   303  	pendingErr := sess.getPendingError()
   304  	if pendingErr != nil {
   305  		return nil, xerrors.Errorf("failed to get a connection from the pool because pending error is found: %w", pendingErr)
   306  	}
   307  
   308  	connections := map[*connection.IRODSConnection]bool{}
   309  
   310  	// check if there are available connections in the pool
   311  	for i := 0; i < number; i++ {
   312  		if sess.connectionPool.AvailableConnections() > 0 {
   313  			// try to get it from the pool
   314  			conn, _, err := sess.connectionPool.Get()
   315  			if err != nil {
   316  				if types.IsConnectionPoolFullError(err) {
   317  					logger.WithError(err).Debug("failed to get a connection from the pool, the pool is full")
   318  					// fall below
   319  				} else {
   320  					// fail
   321  					sess.lastConnectionError = err
   322  					sess.lastConnectionErrorTime = time.Now()
   323  
   324  					return nil, err
   325  				}
   326  				break
   327  			} else {
   328  				connections[conn] = true
   329  
   330  				// put to share
   331  				if shares, ok := sess.sharedConnections[conn]; ok {
   332  					shares++
   333  					sess.sharedConnections[conn] = shares
   334  				} else {
   335  					sess.sharedConnections[conn] = 1
   336  				}
   337  			}
   338  		} else {
   339  			break
   340  		}
   341  	}
   342  
   343  	connectionsInNeed := number - len(connections)
   344  
   345  	// failed to get connection from pool
   346  	// find a connection from shared connection
   347  	logger.Debug("Share an in-use connection as it cannot create a new connection")
   348  	for connectionsInNeed > 0 {
   349  		for sharedConn, shareCount := range sess.sharedConnections {
   350  			shareCount++
   351  
   352  			connections[sharedConn] = true
   353  			sess.sharedConnections[sharedConn] = shareCount
   354  
   355  			connectionsInNeed--
   356  			if connectionsInNeed <= 0 {
   357  				break
   358  			}
   359  		}
   360  	}
   361  
   362  	acquiredConnections := []*connection.IRODSConnection{}
   363  	for conn := range connections {
   364  		acquiredConnections = append(acquiredConnections, conn)
   365  	}
   366  
   367  	if !sess.supportParallelUploadSet {
   368  		if len(acquiredConnections) > 0 {
   369  			sess.supportParallelUpload = acquiredConnections[0].SupportParallelUpload()
   370  			sess.supportParallelUploadSet = true
   371  		}
   372  	}
   373  
   374  	return acquiredConnections, nil
   375  }
   376  
   377  // AcquireUnmanagedConnection returns a connection that is not managed
   378  func (sess *IRODSSession) AcquireUnmanagedConnection() (*connection.IRODSConnection, error) {
   379  	logger := log.WithFields(log.Fields{
   380  		"package":  "session",
   381  		"struct":   "IRODSSession",
   382  		"function": "AcquireUnmanagedConnection",
   383  	})
   384  
   385  	sess.mutex.Lock()
   386  	defer sess.mutex.Unlock()
   387  
   388  	// return last error
   389  	pendingErr := sess.getPendingError()
   390  	if pendingErr != nil {
   391  		return nil, xerrors.Errorf("failed to get a connection because pending error is found: %w", pendingErr)
   392  	}
   393  
   394  	// create a new one
   395  	newConn := connection.NewIRODSConnection(sess.account, sess.config.OperationTimeout, sess.config.ApplicationName)
   396  	err := newConn.Connect()
   397  	if err != nil {
   398  		sess.lastConnectionError = err
   399  		sess.lastConnectionErrorTime = time.Now()
   400  
   401  		return nil, xerrors.Errorf("failed to connect to irods server: %w", err)
   402  	}
   403  
   404  	logger.Debug("Created a new unmanaged connection")
   405  
   406  	if !sess.supportParallelUploadSet {
   407  		sess.supportParallelUpload = newConn.SupportParallelUpload()
   408  		sess.supportParallelUploadSet = true
   409  	}
   410  
   411  	return newConn, nil
   412  }
   413  
   414  // ReturnConnection returns an idle connection with transaction close
   415  func (sess *IRODSSession) ReturnConnection(conn *connection.IRODSConnection) error {
   416  	logger := log.WithFields(log.Fields{
   417  		"package":  "session",
   418  		"struct":   "IRODSSession",
   419  		"function": "ReturnConnection",
   420  	})
   421  
   422  	sess.mutex.Lock()
   423  	defer sess.mutex.Unlock()
   424  
   425  	if share, ok := sess.sharedConnections[conn]; ok {
   426  		share--
   427  		if share <= 0 {
   428  			// no share
   429  			delete(sess.sharedConnections, conn)
   430  
   431  			conn.Lock()
   432  			if conn.IsTransactionDirty() {
   433  				err := sess.endTransaction(conn)
   434  				if err != nil {
   435  					conn.Unlock()
   436  
   437  					logger.Debug(err)
   438  
   439  					// discard, since we cannot reuse the connection
   440  					sess.connectionPool.Discard(conn)
   441  					return nil
   442  				}
   443  
   444  				// clear transaction
   445  				conn.SetTransactionDirty(false)
   446  			}
   447  			conn.Unlock()
   448  
   449  			err := sess.connectionPool.Return(conn)
   450  			if err != nil {
   451  				return xerrors.Errorf("failed to return an idle connection: %w", err)
   452  			}
   453  		} else {
   454  			sess.sharedConnections[conn] = share
   455  		}
   456  	} else {
   457  		// may be unmanged?
   458  		if conn.IsConnected() {
   459  			conn.Disconnect()
   460  		}
   461  	}
   462  
   463  	return nil
   464  }
   465  
   466  // DiscardConnection discards a connection
   467  func (sess *IRODSSession) DiscardConnection(conn *connection.IRODSConnection) error {
   468  	sess.mutex.Lock()
   469  	defer sess.mutex.Unlock()
   470  
   471  	if share, ok := sess.sharedConnections[conn]; ok {
   472  		share--
   473  		if share <= 0 {
   474  			// no share
   475  			delete(sess.sharedConnections, conn)
   476  
   477  			sess.connectionPool.Discard(conn)
   478  			return nil
   479  		} else {
   480  			sess.sharedConnections[conn] = share
   481  		}
   482  	} else {
   483  		// may be unmanaged?
   484  		if conn.IsConnected() {
   485  			conn.Disconnect()
   486  		}
   487  	}
   488  
   489  	return nil
   490  }
   491  
   492  // Release releases all connections
   493  func (sess *IRODSSession) Release() {
   494  	sess.mutex.Lock()
   495  	defer sess.mutex.Unlock()
   496  
   497  	// we don't disconnect connections here,
   498  	// we will disconnect it when calling pool.Release
   499  	sess.sharedConnections = map[*connection.IRODSConnection]int{}
   500  
   501  	sess.lastConnectionError = nil
   502  
   503  	sess.connectionPool.Release()
   504  }
   505  
   506  // SupportParallelUpload returns if parallel upload is supported
   507  func (sess *IRODSSession) SupportParallelUpload() bool {
   508  	logger := log.WithFields(log.Fields{
   509  		"package":  "session",
   510  		"function": "SupportParallelUpload",
   511  	})
   512  
   513  	sess.mutex.Lock()
   514  	defer sess.mutex.Unlock()
   515  
   516  	// return last error
   517  	pendingErr := sess.getPendingError()
   518  	if pendingErr != nil {
   519  		return false
   520  	}
   521  
   522  	if !sess.supportParallelUploadSet {
   523  		conn, _, err := sess.connectionPool.Get()
   524  		if err != nil {
   525  			if !types.IsConnectionPoolFullError(err) {
   526  				sess.lastConnectionError = err
   527  				sess.lastConnectionErrorTime = time.Now()
   528  			}
   529  
   530  			return false
   531  		}
   532  
   533  		conn.Lock()
   534  
   535  		// check parallel upload
   536  		sess.supportParallelUpload = conn.SupportParallelUpload()
   537  		logger.Debugf("support parallel upload: %t", sess.supportParallelUpload)
   538  
   539  		conn.Unlock()
   540  
   541  		sess.connectionPool.Return(conn)
   542  		sess.supportParallelUploadSet = true
   543  	}
   544  
   545  	return sess.supportParallelUpload
   546  }
   547  
   548  // Connections returns the number of connections in the pool
   549  func (sess *IRODSSession) ConnectionTotal() int {
   550  	sess.mutex.Lock()
   551  	defer sess.mutex.Unlock()
   552  
   553  	return sess.connectionPool.OpenConnections()
   554  }
   555  
   556  // GetMetrics returns metrics
   557  func (sess *IRODSSession) GetMetrics() *metrics.IRODSMetrics {
   558  	return &sess.metrics
   559  }