github.com/ydb-platform/ydb-go-sdk/v3@v3.89.2/internal/query/transaction.go (about)

     1  package query
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  
     7  	"github.com/ydb-platform/ydb-go-genproto/Ydb_Query_V1"
     8  	"github.com/ydb-platform/ydb-go-genproto/protos/Ydb"
     9  	"github.com/ydb-platform/ydb-go-genproto/protos/Ydb_Query"
    10  
    11  	"github.com/ydb-platform/ydb-go-sdk/v3/internal/allocator"
    12  	"github.com/ydb-platform/ydb-go-sdk/v3/internal/query/options"
    13  	"github.com/ydb-platform/ydb-go-sdk/v3/internal/query/result"
    14  	"github.com/ydb-platform/ydb-go-sdk/v3/internal/query/session"
    15  	queryTx "github.com/ydb-platform/ydb-go-sdk/v3/internal/query/tx"
    16  	"github.com/ydb-platform/ydb-go-sdk/v3/internal/stack"
    17  	baseTx "github.com/ydb-platform/ydb-go-sdk/v3/internal/tx"
    18  	"github.com/ydb-platform/ydb-go-sdk/v3/internal/xerrors"
    19  	"github.com/ydb-platform/ydb-go-sdk/v3/internal/xsync"
    20  	"github.com/ydb-platform/ydb-go-sdk/v3/query"
    21  	"github.com/ydb-platform/ydb-go-sdk/v3/trace"
    22  )
    23  
    24  var (
    25  	_ query.Transaction  = (*Transaction)(nil)
    26  	_ baseTx.Transaction = (*Transaction)(nil)
    27  )
    28  
    29  type (
    30  	Transaction struct {
    31  		baseTx.LazyID
    32  
    33  		s          *Session
    34  		txSettings query.TransactionSettings
    35  
    36  		completed bool
    37  
    38  		onBeforeCommit xsync.Set[*baseTx.OnTransactionBeforeCommit]
    39  		onCompleted    xsync.Set[*baseTx.OnTransactionCompletedFunc]
    40  	}
    41  )
    42  
    43  func begin(
    44  	ctx context.Context,
    45  	client Ydb_Query_V1.QueryServiceClient,
    46  	sessionID string,
    47  	txSettings query.TransactionSettings,
    48  ) (txID string, _ error) {
    49  	a := allocator.New()
    50  	defer a.Free()
    51  	response, err := client.BeginTransaction(ctx,
    52  		&Ydb_Query.BeginTransactionRequest{
    53  			SessionId:  sessionID,
    54  			TxSettings: txSettings.ToYDB(a),
    55  		},
    56  	)
    57  	if err != nil {
    58  		return "", xerrors.WithStackTrace(err)
    59  	}
    60  
    61  	return response.GetTxMeta().GetId(), nil
    62  }
    63  
    64  func (tx *Transaction) UnLazy(ctx context.Context) error {
    65  	if tx.ID() != baseTx.LazyTxID {
    66  		return nil
    67  	}
    68  
    69  	txID, err := begin(ctx, tx.s.client, tx.s.ID(), tx.txSettings)
    70  	if err != nil {
    71  		return xerrors.WithStackTrace(err)
    72  	}
    73  
    74  	tx.SetTxID(txID)
    75  
    76  	return nil
    77  }
    78  
    79  func (tx *Transaction) QueryResultSet(
    80  	ctx context.Context, q string, opts ...options.Execute,
    81  ) (rs result.ClosableResultSet, finalErr error) {
    82  	onDone := trace.QueryOnTxQueryResultSet(tx.s.trace, &ctx,
    83  		stack.FunctionID("github.com/ydb-platform/ydb-go-sdk/v3/internal/query.(*Transaction).QueryResultSet"), tx, q)
    84  	defer func() {
    85  		onDone(finalErr)
    86  	}()
    87  
    88  	if tx.completed {
    89  		return nil, xerrors.WithStackTrace(errExecuteOnCompletedTx)
    90  	}
    91  
    92  	settings, err := tx.executeSettings(opts...)
    93  	if err != nil {
    94  		return nil, xerrors.WithStackTrace(err)
    95  	}
    96  
    97  	resultOpts := []resultOption{
    98  		withTrace(tx.s.trace),
    99  		onTxMeta(func(txMeta *Ydb_Query.TransactionMeta) {
   100  			tx.SetTxID(txMeta.GetId())
   101  		}),
   102  	}
   103  	if settings.TxControl().Commit {
   104  		err = tx.waitOnBeforeCommit(ctx)
   105  		if err != nil {
   106  			return nil, err
   107  		}
   108  
   109  		// notification about complete transaction must be sended for any error or for successfully read all result if
   110  		// it was execution with commit flag
   111  		resultOpts = append(resultOpts,
   112  			onNextPartErr(func(err error) {
   113  				tx.notifyOnCompleted(xerrors.HideEOF(err))
   114  			}),
   115  		)
   116  	}
   117  	r, err := execute(ctx, tx.s.ID(), tx.s.client, q, settings, resultOpts...)
   118  	if err != nil {
   119  		return nil, xerrors.WithStackTrace(err)
   120  	}
   121  
   122  	rs, err = readResultSet(ctx, r)
   123  	if err != nil {
   124  		return nil, xerrors.WithStackTrace(err)
   125  	}
   126  
   127  	return rs, nil
   128  }
   129  
   130  func (tx *Transaction) QueryRow(
   131  	ctx context.Context, q string, opts ...options.Execute,
   132  ) (row query.Row, finalErr error) {
   133  	onDone := trace.QueryOnTxQueryRow(tx.s.trace, &ctx,
   134  		stack.FunctionID("github.com/ydb-platform/ydb-go-sdk/v3/internal/query.(*Transaction).QueryRow"), tx, q)
   135  	defer func() {
   136  		onDone(finalErr)
   137  	}()
   138  
   139  	settings := options.ExecuteSettings(
   140  		append(
   141  			[]options.Execute{options.WithTxControl(tx.txControl())},
   142  			opts...,
   143  		)...,
   144  	)
   145  
   146  	resultOpts := []resultOption{
   147  		withTrace(tx.s.trace),
   148  		onTxMeta(func(txMeta *Ydb_Query.TransactionMeta) {
   149  			tx.SetTxID(txMeta.GetId())
   150  		}),
   151  	}
   152  	if settings.TxControl().Commit {
   153  		err := tx.waitOnBeforeCommit(ctx)
   154  		if err != nil {
   155  			return nil, err
   156  		}
   157  
   158  		// notification about complete transaction must be sended for any error or for successfully read all result if
   159  		// it was execution with commit flag
   160  		resultOpts = append(resultOpts,
   161  			onNextPartErr(func(err error) {
   162  				tx.notifyOnCompleted(xerrors.HideEOF(err))
   163  			}),
   164  		)
   165  	}
   166  	r, err := execute(ctx, tx.s.ID(), tx.s.client, q, settings, resultOpts...)
   167  	if err != nil {
   168  		return nil, xerrors.WithStackTrace(err)
   169  	}
   170  
   171  	row, err = readRow(ctx, r)
   172  	if err != nil {
   173  		return nil, xerrors.WithStackTrace(err)
   174  	}
   175  
   176  	return row, nil
   177  }
   178  
   179  func (tx *Transaction) SessionID() string {
   180  	return tx.s.ID()
   181  }
   182  
   183  func (tx *Transaction) txControl() *queryTx.Control {
   184  	if tx.ID() != baseTx.LazyTxID {
   185  		return queryTx.NewControl(queryTx.WithTxID(tx.ID()))
   186  	}
   187  
   188  	return queryTx.NewControl(
   189  		queryTx.BeginTx(tx.txSettings...),
   190  	)
   191  }
   192  
   193  func (tx *Transaction) Exec(ctx context.Context, q string, opts ...options.Execute) (
   194  	finalErr error,
   195  ) {
   196  	onDone := trace.QueryOnTxExec(tx.s.trace, &ctx,
   197  		stack.FunctionID("github.com/ydb-platform/ydb-go-sdk/v3/internal/query.(*Transaction).Exec"), tx.s, tx, q)
   198  	defer func() {
   199  		onDone(finalErr)
   200  	}()
   201  
   202  	if tx.completed {
   203  		return xerrors.WithStackTrace(errExecuteOnCompletedTx)
   204  	}
   205  
   206  	settings, err := tx.executeSettings(opts...)
   207  	if err != nil {
   208  		return xerrors.WithStackTrace(err)
   209  	}
   210  
   211  	resultOpts := []resultOption{
   212  		withTrace(tx.s.trace),
   213  		onTxMeta(func(txMeta *Ydb_Query.TransactionMeta) {
   214  			tx.SetTxID(txMeta.GetId())
   215  		}),
   216  	}
   217  	if settings.TxControl().Commit {
   218  		err = tx.waitOnBeforeCommit(ctx)
   219  		if err != nil {
   220  			return err
   221  		}
   222  
   223  		// notification about complete transaction must be sended for any error or for successfully read all result if
   224  		// it was execution with commit flag
   225  		resultOpts = append(resultOpts,
   226  			onNextPartErr(func(err error) {
   227  				tx.notifyOnCompleted(xerrors.HideEOF(err))
   228  			}),
   229  		)
   230  	}
   231  
   232  	r, err := execute(ctx, tx.s.ID(), tx.s.client, q, settings, resultOpts...)
   233  	if err != nil {
   234  		return xerrors.WithStackTrace(err)
   235  	}
   236  
   237  	err = readAll(ctx, r)
   238  	if err != nil {
   239  		return xerrors.WithStackTrace(err)
   240  	}
   241  
   242  	return nil
   243  }
   244  
   245  func (tx *Transaction) executeSettings(opts ...options.Execute) (_ executeSettings, finalErr error) {
   246  	for _, opt := range opts {
   247  		if opt == nil {
   248  			return nil, xerrors.WithStackTrace(errNilOption)
   249  		}
   250  		if _, has := opt.(options.ExecuteNoTx); has {
   251  			return nil, xerrors.WithStackTrace(
   252  				fmt.Errorf("%T: %w", opt, ErrOptionNotForTxExecute),
   253  			)
   254  		}
   255  	}
   256  
   257  	return options.ExecuteSettings(append([]options.Execute{
   258  		options.WithTxControl(tx.txControl()),
   259  	}, opts...)...), nil
   260  }
   261  
   262  func (tx *Transaction) Query(ctx context.Context, q string, opts ...options.Execute) (
   263  	_ query.Result, finalErr error,
   264  ) {
   265  	onDone := trace.QueryOnTxQuery(tx.s.trace, &ctx,
   266  		stack.FunctionID("github.com/ydb-platform/ydb-go-sdk/v3/internal/query.(*Transaction).Query"), tx.s, tx, q)
   267  	defer func() {
   268  		onDone(finalErr)
   269  	}()
   270  
   271  	if tx.completed {
   272  		return nil, xerrors.WithStackTrace(errExecuteOnCompletedTx)
   273  	}
   274  
   275  	settings, err := tx.executeSettings(opts...)
   276  	if err != nil {
   277  		return nil, xerrors.WithStackTrace(err)
   278  	}
   279  
   280  	resultOpts := []resultOption{
   281  		withTrace(tx.s.trace),
   282  		onTxMeta(func(txMeta *Ydb_Query.TransactionMeta) {
   283  			tx.SetTxID(txMeta.GetId())
   284  		}),
   285  	}
   286  	if settings.TxControl().Commit {
   287  		err = tx.waitOnBeforeCommit(ctx)
   288  		if err != nil {
   289  			return nil, err
   290  		}
   291  
   292  		// notification about complete transaction must be sended for any error or for successfully read all result if
   293  		// it was execution with commit flag
   294  		resultOpts = append(resultOpts,
   295  			onNextPartErr(func(err error) {
   296  				tx.notifyOnCompleted(xerrors.HideEOF(err))
   297  			}),
   298  		)
   299  	}
   300  	r, err := execute(ctx, tx.s.ID(), tx.s.client, q, settings, resultOpts...)
   301  	if err != nil {
   302  		return nil, xerrors.WithStackTrace(err)
   303  	}
   304  
   305  	return r, nil
   306  }
   307  
   308  func commitTx(ctx context.Context, client Ydb_Query_V1.QueryServiceClient, sessionID, txID string) error {
   309  	_, err := client.CommitTransaction(ctx, &Ydb_Query.CommitTransactionRequest{
   310  		SessionId: sessionID,
   311  		TxId:      txID,
   312  	})
   313  	if err != nil {
   314  		return xerrors.WithStackTrace(err)
   315  	}
   316  
   317  	return nil
   318  }
   319  
   320  func (tx *Transaction) CommitTx(ctx context.Context) (finalErr error) {
   321  	if tx.ID() == baseTx.LazyTxID {
   322  		return nil
   323  	}
   324  
   325  	if tx.completed {
   326  		return nil
   327  	}
   328  
   329  	defer func() {
   330  		tx.notifyOnCompleted(finalErr)
   331  		tx.completed = true
   332  	}()
   333  
   334  	err := tx.waitOnBeforeCommit(ctx)
   335  	if err != nil {
   336  		return err
   337  	}
   338  
   339  	err = commitTx(ctx, tx.s.client, tx.s.ID(), tx.ID())
   340  	if err != nil {
   341  		if xerrors.IsOperationError(err, Ydb.StatusIds_BAD_SESSION) {
   342  			tx.s.SetStatus(session.StatusClosed)
   343  		}
   344  
   345  		return xerrors.WithStackTrace(err)
   346  	}
   347  
   348  	return nil
   349  }
   350  
   351  func rollback(ctx context.Context, client Ydb_Query_V1.QueryServiceClient, sessionID, txID string) error {
   352  	_, err := client.RollbackTransaction(ctx, &Ydb_Query.RollbackTransactionRequest{
   353  		SessionId: sessionID,
   354  		TxId:      txID,
   355  	})
   356  	if err != nil {
   357  		return xerrors.WithStackTrace(err)
   358  	}
   359  
   360  	return nil
   361  }
   362  
   363  func (tx *Transaction) Rollback(ctx context.Context) (finalErr error) {
   364  	if tx.ID() == baseTx.LazyTxID {
   365  		// https://github.com/ydb-platform/ydb-go-sdk/issues/1456
   366  		return tx.s.Close(ctx)
   367  	}
   368  
   369  	if tx.completed {
   370  		return nil
   371  	}
   372  
   373  	tx.completed = true
   374  
   375  	tx.notifyOnCompleted(ErrTransactionRollingBack)
   376  
   377  	err := rollback(ctx, tx.s.client, tx.s.ID(), tx.ID())
   378  	if err != nil {
   379  		if xerrors.IsOperationError(err, Ydb.StatusIds_BAD_SESSION) {
   380  			tx.s.SetStatus(session.StatusClosed)
   381  		}
   382  
   383  		return xerrors.WithStackTrace(err)
   384  	}
   385  
   386  	return nil
   387  }
   388  
   389  func (tx *Transaction) OnBeforeCommit(f baseTx.OnTransactionBeforeCommit) {
   390  	tx.onBeforeCommit.Add(&f)
   391  }
   392  
   393  func (tx *Transaction) OnCompleted(f baseTx.OnTransactionCompletedFunc) {
   394  	tx.onCompleted.Add(&f)
   395  }
   396  
   397  func (tx *Transaction) waitOnBeforeCommit(ctx context.Context) (resErr error) {
   398  	tx.onBeforeCommit.Range(func(f *baseTx.OnTransactionBeforeCommit) bool {
   399  		resErr = (*f)(ctx)
   400  
   401  		return resErr == nil
   402  	})
   403  
   404  	return resErr
   405  }
   406  
   407  func (tx *Transaction) notifyOnCompleted(err error) {
   408  	tx.completed = true
   409  
   410  	tx.onCompleted.Range(func(f *baseTx.OnTransactionCompletedFunc) bool {
   411  		(*f)(err)
   412  
   413  		return tx.onCompleted.Remove(f)
   414  	})
   415  }