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

     1  package table
     2  
     3  import (
     4  	"context"
     5  
     6  	"github.com/jonboulle/clockwork"
     7  	"github.com/ydb-platform/ydb-go-genproto/Ydb_Table_V1"
     8  	"google.golang.org/grpc"
     9  
    10  	"github.com/ydb-platform/ydb-go-sdk/v3/internal/allocator"
    11  	"github.com/ydb-platform/ydb-go-sdk/v3/internal/pool"
    12  	"github.com/ydb-platform/ydb-go-sdk/v3/internal/stack"
    13  	"github.com/ydb-platform/ydb-go-sdk/v3/internal/table/config"
    14  	"github.com/ydb-platform/ydb-go-sdk/v3/internal/xcontext"
    15  	"github.com/ydb-platform/ydb-go-sdk/v3/internal/xerrors"
    16  	"github.com/ydb-platform/ydb-go-sdk/v3/retry"
    17  	"github.com/ydb-platform/ydb-go-sdk/v3/table"
    18  	"github.com/ydb-platform/ydb-go-sdk/v3/trace"
    19  )
    20  
    21  // sessionBuilder is the interface that holds logic of creating sessions.
    22  type sessionBuilder func(ctx context.Context) (*session, error)
    23  
    24  func New(ctx context.Context, cc grpc.ClientConnInterface, config *config.Config) *Client {
    25  	onDone := trace.TableOnInit(config.Trace(), &ctx,
    26  		stack.FunctionID("github.com/ydb-platform/ydb-go-sdk/v3/internal/table.New"),
    27  	)
    28  
    29  	return &Client{
    30  		clock:  config.Clock(),
    31  		config: config,
    32  		cc:     cc,
    33  		build: func(ctx context.Context) (s *session, err error) {
    34  			return newSession(ctx, cc, config)
    35  		},
    36  		pool: pool.New[*session, session](ctx,
    37  			pool.WithLimit[*session, session](config.SizeLimit()),
    38  			pool.WithItemUsageLimit[*session, session](config.SessionUsageLimit()),
    39  			pool.WithIdleTimeToLive[*session, session](config.IdleThreshold()),
    40  			pool.WithCreateItemTimeout[*session, session](config.CreateSessionTimeout()),
    41  			pool.WithCloseItemTimeout[*session, session](config.DeleteTimeout()),
    42  			pool.WithClock[*session, session](config.Clock()),
    43  			pool.WithCreateItemFunc[*session, session](func(ctx context.Context) (*session, error) {
    44  				return newSession(ctx, cc, config)
    45  			}),
    46  			pool.WithTrace[*session, session](&pool.Trace{
    47  				OnNew: func(ctx *context.Context, call stack.Caller) func(limit int) {
    48  					return func(limit int) {
    49  						onDone(limit)
    50  					}
    51  				},
    52  				OnPut: func(ctx *context.Context, call stack.Caller, item any) func(err error) {
    53  					onDone := trace.TableOnPoolPut( //nolint:forcetypeassert
    54  						config.Trace(), ctx, call, item.(*session),
    55  					)
    56  
    57  					return func(err error) {
    58  						onDone(err)
    59  					}
    60  				},
    61  				OnGet: func(ctx *context.Context, call stack.Caller) func(item any, attempts int, err error) {
    62  					onDone := trace.TableOnPoolGet(config.Trace(), ctx, call)
    63  
    64  					return func(item any, attempts int, err error) {
    65  						onDone(item.(*session), attempts, err) //nolint:forcetypeassert
    66  					}
    67  				},
    68  				OnWith: func(ctx *context.Context, call stack.Caller) func(attempts int, err error) {
    69  					onDone := trace.TableOnPoolWith(config.Trace(), ctx, call)
    70  
    71  					return func(attempts int, err error) {
    72  						onDone(attempts, err)
    73  					}
    74  				},
    75  				OnChange: func(stats pool.Stats) {
    76  					trace.TableOnPoolStateChange(config.Trace(),
    77  						stats.Limit, stats.Index, stats.Idle, stats.Wait, stats.CreateInProgress, stats.Index,
    78  					)
    79  				},
    80  			}),
    81  		),
    82  		done: make(chan struct{}),
    83  	}
    84  }
    85  
    86  // Client is a set of session instances that may be reused.
    87  // A Client is safe for use by multiple goroutines simultaneously.
    88  type Client struct {
    89  	// read-only fields
    90  	config *config.Config
    91  	build  sessionBuilder
    92  	cc     grpc.ClientConnInterface
    93  	clock  clockwork.Clock
    94  	pool   sessionPool
    95  	done   chan struct{}
    96  }
    97  
    98  func (c *Client) CreateSession(ctx context.Context, opts ...table.Option) (_ table.ClosableSession, err error) {
    99  	if c == nil {
   100  		return nil, xerrors.WithStackTrace(errNilClient)
   101  	}
   102  	if c.isClosed() {
   103  		return nil, xerrors.WithStackTrace(errClosedClient)
   104  	}
   105  	createSession := func(ctx context.Context) (*session, error) {
   106  		s, err := c.build(ctx)
   107  		if err != nil {
   108  			return nil, xerrors.WithStackTrace(err)
   109  		}
   110  
   111  		return s, nil
   112  	}
   113  	if !c.config.AutoRetry() {
   114  		s, err := createSession(ctx)
   115  		if err != nil {
   116  			return nil, xerrors.WithStackTrace(err)
   117  		}
   118  
   119  		return s, nil
   120  	}
   121  
   122  	var (
   123  		onDone = trace.TableOnCreateSession(c.config.Trace(), &ctx,
   124  			stack.FunctionID(
   125  				"github.com/ydb-platform/ydb-go-sdk/v3/internal/table.(*Client).CreateSession"),
   126  		)
   127  		attempts = 0
   128  		s        *session
   129  	)
   130  	defer func() {
   131  		if s != nil {
   132  			onDone(s, attempts, err)
   133  		} else {
   134  			onDone(nil, attempts, err)
   135  		}
   136  	}()
   137  
   138  	s, err = retry.RetryWithResult(ctx, createSession,
   139  		append(
   140  			[]retry.Option{
   141  				retry.WithIdempotent(true),
   142  				retry.WithTrace(&trace.Retry{
   143  					OnRetry: func(info trace.RetryLoopStartInfo) func(trace.RetryLoopDoneInfo) {
   144  						return func(info trace.RetryLoopDoneInfo) {
   145  							attempts = info.Attempts
   146  						}
   147  					},
   148  				}),
   149  			}, c.retryOptions(opts...).RetryOptions...,
   150  		)...,
   151  	)
   152  	if err != nil {
   153  		return nil, xerrors.WithStackTrace(err)
   154  	}
   155  
   156  	return s, nil
   157  }
   158  
   159  func (c *Client) isClosed() bool {
   160  	select {
   161  	case <-c.done:
   162  		return true
   163  	default:
   164  		return false
   165  	}
   166  }
   167  
   168  // Close deletes all stored sessions inside Client.
   169  // It also stops all underlying timers and goroutines.
   170  // It returns first error occurred during stale sessions' deletion.
   171  // Note that even on error it calls Close() on each session.
   172  func (c *Client) Close(ctx context.Context) (err error) {
   173  	if c == nil {
   174  		return xerrors.WithStackTrace(errNilClient)
   175  	}
   176  
   177  	close(c.done)
   178  
   179  	onDone := trace.TableOnClose(c.config.Trace(), &ctx,
   180  		stack.FunctionID("github.com/ydb-platform/ydb-go-sdk/v3/internal/table.(*Client).Close"),
   181  	)
   182  	defer func() {
   183  		onDone(err)
   184  	}()
   185  
   186  	return c.pool.Close(ctx)
   187  }
   188  
   189  // Do provide the best effort for execute operation
   190  // Do implements internal busy loop until one of the following conditions is met:
   191  // - deadline was canceled or deadlined
   192  // - retry operation returned nil as error
   193  // Warning: if deadline without deadline or cancellation func Retry will be worked infinite
   194  func (c *Client) Do(ctx context.Context, op table.Operation, opts ...table.Option) (finalErr error) {
   195  	if c == nil {
   196  		return xerrors.WithStackTrace(errNilClient)
   197  	}
   198  
   199  	if c.isClosed() {
   200  		return xerrors.WithStackTrace(errClosedClient)
   201  	}
   202  
   203  	config := c.retryOptions(opts...)
   204  
   205  	attempts, onDone := 0, trace.TableOnDo(config.Trace, &ctx,
   206  		stack.FunctionID("github.com/ydb-platform/ydb-go-sdk/v3/internal/table.(*Client).Do"),
   207  		config.Label, config.Idempotent, xcontext.IsNestedCall(ctx),
   208  	)
   209  	defer func() {
   210  		onDone(attempts, finalErr)
   211  	}()
   212  
   213  	err := do(ctx, c.pool, c.config, op, func(err error) {
   214  		attempts++
   215  	}, config.RetryOptions...)
   216  	if err != nil {
   217  		return xerrors.WithStackTrace(err)
   218  	}
   219  
   220  	return nil
   221  }
   222  
   223  func (c *Client) DoTx(ctx context.Context, op table.TxOperation, opts ...table.Option) (finalErr error) {
   224  	if c == nil {
   225  		return xerrors.WithStackTrace(errNilClient)
   226  	}
   227  
   228  	if c.isClosed() {
   229  		return xerrors.WithStackTrace(errClosedClient)
   230  	}
   231  
   232  	config := c.retryOptions(opts...)
   233  
   234  	attempts, onDone := 0, trace.TableOnDoTx(config.Trace, &ctx,
   235  		stack.FunctionID("github.com/ydb-platform/ydb-go-sdk/v3/internal/table.(*Client).DoTx"),
   236  		config.Label, config.Idempotent, xcontext.IsNestedCall(ctx),
   237  	)
   238  	defer func() {
   239  		onDone(attempts, finalErr)
   240  	}()
   241  
   242  	return retryBackoff(ctx, c.pool, func(ctx context.Context, s table.Session) (err error) {
   243  		attempts++
   244  
   245  		tx, err := s.BeginTransaction(ctx, config.TxSettings)
   246  		if err != nil {
   247  			return xerrors.WithStackTrace(err)
   248  		}
   249  
   250  		defer func() {
   251  			if err != nil && !xerrors.IsOperationError(err) {
   252  				_ = tx.Rollback(ctx)
   253  			}
   254  		}()
   255  
   256  		if err = executeTxOperation(ctx, c, op, tx); err != nil {
   257  			return xerrors.WithStackTrace(err)
   258  		}
   259  
   260  		_, err = tx.CommitTx(ctx, config.TxCommitOptions...)
   261  		if err != nil {
   262  			return xerrors.WithStackTrace(err)
   263  		}
   264  
   265  		return nil
   266  	}, config.RetryOptions...)
   267  }
   268  
   269  func (c *Client) BulkUpsert(
   270  	ctx context.Context,
   271  	tableName string,
   272  	data table.BulkUpsertData,
   273  	opts ...table.Option,
   274  ) (finalErr error) {
   275  	if c == nil {
   276  		return xerrors.WithStackTrace(errNilClient)
   277  	}
   278  
   279  	if c.isClosed() {
   280  		return xerrors.WithStackTrace(errClosedClient)
   281  	}
   282  
   283  	a := allocator.New()
   284  	defer a.Free()
   285  
   286  	attempts, config := 0, c.retryOptions(opts...)
   287  	config.RetryOptions = append(config.RetryOptions,
   288  		retry.WithIdempotent(true),
   289  		retry.WithTrace(&trace.Retry{
   290  			OnRetry: func(info trace.RetryLoopStartInfo) func(trace.RetryLoopDoneInfo) {
   291  				return func(info trace.RetryLoopDoneInfo) {
   292  					attempts = info.Attempts
   293  				}
   294  			},
   295  		}),
   296  	)
   297  
   298  	onDone := trace.TableOnBulkUpsert(config.Trace, &ctx,
   299  		stack.FunctionID("github.com/ydb-platform/ydb-go-sdk/v3/internal/table.(*Client).BulkUpsert"),
   300  	)
   301  	defer func() {
   302  		onDone(finalErr, attempts)
   303  	}()
   304  
   305  	request, err := data.ToYDB(a, tableName)
   306  	if err != nil {
   307  		return xerrors.WithStackTrace(err)
   308  	}
   309  
   310  	client := Ydb_Table_V1.NewTableServiceClient(c.cc)
   311  
   312  	err = retry.Retry(ctx,
   313  		func(ctx context.Context) (err error) {
   314  			attempts++
   315  			_, err = client.BulkUpsert(ctx, request)
   316  
   317  			return err
   318  		},
   319  		config.RetryOptions...,
   320  	)
   321  	if err != nil {
   322  		return xerrors.WithStackTrace(err)
   323  	}
   324  
   325  	return nil
   326  }
   327  
   328  func executeTxOperation(ctx context.Context, c *Client, op table.TxOperation, tx table.Transaction) (err error) {
   329  	if panicCallback := c.config.PanicCallback(); panicCallback != nil {
   330  		defer func() {
   331  			if e := recover(); e != nil {
   332  				panicCallback(e)
   333  			}
   334  		}()
   335  	}
   336  
   337  	return op(xcontext.MarkRetryCall(ctx), tx)
   338  }