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 }