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 }