github.com/NVIDIA/aistore@v1.3.23-0.20240517131212-7df6609be51d/api/object.go (about) 1 // Package api provides native Go-based API/SDK over HTTP(S). 2 /* 3 * Copyright (c) 2018-2024, NVIDIA CORPORATION. All rights reserved. 4 */ 5 package api 6 7 import ( 8 "encoding/hex" 9 "fmt" 10 "io" 11 "net/http" 12 "net/textproto" 13 "net/url" 14 "strconv" 15 "time" 16 17 "github.com/NVIDIA/aistore/api/apc" 18 "github.com/NVIDIA/aistore/cmn" 19 "github.com/NVIDIA/aistore/cmn/cos" 20 ) 21 22 const ( 23 httpMaxRetries = 5 // maximum number of retries for an HTTP request 24 httpRetrySleep = 100 * time.Millisecond // a sleep between HTTP request retries 25 26 // Sleep between HTTP retries for error[rate of change requests exceeds limit] - must be > 1s: 27 // From https://cloud.google.com/storage/quotas#objects 28 // * "There is an update limit on each object of once per second..." 29 httpRetryRateSleep = 1500 * time.Millisecond 30 ) 31 32 // GET (object) 33 type ( 34 GetArgs struct { 35 // If not specified (or same: if `nil`), Writer defaults to `io.Discard` 36 // (in other words, with no writer the object that is being read will be discarded) 37 Writer io.Writer 38 39 // Currently, this (optional) Query field can (optionally) carry: 40 // - `apc.QparamETLName`: named ETL to transform the object (i.e., perform "inline transformation") 41 // - `apc.QparamOrigURL`: GET from a vanilla http(s) location (`ht://` bucket with the corresponding `OrigURLBck`) 42 // - `apc.QparamSilent`: do not log errors 43 // - `apc.QparamLatestVer`: get latest version from the associated Cloud bucket; see also: `ValidateWarmGet` 44 // - and a group of parameters used to read aistore-supported serialized archives ("shards"), namely: 45 // - `apc.QparamArchpath` 46 // - `apc.QparamArchmime` 47 // - `apc.QparamArchregx` 48 // - `apc.QparamArchmode` 49 Query url.Values 50 51 // The field is used to facilitate a) range read, and b) blob download 52 // E.g. range: 53 // * Header.Set(cos.HdrRange, fmt.Sprintf("bytes=%d-%d", fromOffset, toOffset)) 54 // For range formatting, see https://www.rfc-editor.org/rfc/rfc7233#section-2.1 55 // E.g. blob download: 56 // * Header.Set(apc.HdrBlobDownload, "true") 57 Header http.Header 58 } 59 60 // `ObjAttrs` represents object attributes and can be further used to retrieve 61 // the object's size, checksum, version, and other metadata. 62 // 63 // Note that while `GetObject()` and related GET APIs return `ObjAttrs`, 64 // `HeadObject()` API returns `cmn.ObjectProps` - a superset. 65 ObjAttrs struct { 66 wrespHeader http.Header 67 n int64 68 } 69 ) 70 71 // PUT, APPEND, PROMOTE (object) 72 type ( 73 NewRequestCB func(args *cmn.HreqArgs) (*http.Request, error) 74 75 PutArgs struct { 76 Reader cos.ReadOpenCloser 77 78 // optional; if provided: 79 // - if object exists: load the object's metadata, compare checksums - skip writing if equal 80 // - otherwise, compare the two checksums upon writing (aka, "end-to-end protection") 81 Cksum *cos.Cksum 82 83 BaseParams BaseParams 84 85 Bck cmn.Bck 86 ObjName string 87 88 Size uint64 // optional 89 90 // Skip loading existing object's metadata in order to 91 // compare its Checksum and update its existing Version (if exists); 92 // can be used to reduce PUT latency when: 93 // - we massively write a new content into a bucket, and/or 94 // - we simply don't care. 95 SkipVC bool 96 } 97 98 // (see also: api.PutApndArchArgs) 99 AppendArgs struct { 100 Reader cos.ReadOpenCloser 101 BaseParams BaseParams 102 Bck cmn.Bck 103 Object string 104 Handle string 105 Size int64 106 } 107 FlushArgs struct { 108 Cksum *cos.Cksum 109 BaseParams BaseParams 110 Bck cmn.Bck 111 Object string 112 Handle string 113 } 114 ) 115 116 // Archive files and directories (see related: cmn.ArchiveBckMsg) 117 type PutApndArchArgs struct { 118 ArchPath string // filename _in_ archive 119 Mime string // user-specified mime type (NOTE: takes precedence if defined) 120 Flags int64 // apc.ArchAppend and apc.ArchAppendIfExist (the former requires destination shard to exist) 121 PutArgs 122 } 123 124 ///////////// 125 // GetArgs // 126 ///////////// 127 128 func (args *GetArgs) ret() (w io.Writer, q url.Values, hdr http.Header) { 129 w = io.Discard 130 if args == nil { 131 return 132 } 133 if args.Writer != nil { 134 w = args.Writer 135 } 136 q, hdr = args.Query, args.Header 137 return 138 } 139 140 ////////////// 141 // ObjAttrs // 142 ////////////// 143 144 // most often used (convenience) method 145 func (oah *ObjAttrs) Size() int64 { 146 if oah.n == 0 { // unlikely 147 oah.n = oah.Attrs().Size 148 } 149 return oah.n 150 } 151 152 func (oah *ObjAttrs) Attrs() (out cmn.ObjAttrs) { 153 out.Cksum = out.FromHeader(oah.wrespHeader) 154 return 155 } 156 157 // e.g. usage: range read response 158 func (oah *ObjAttrs) RespHeader() http.Header { 159 return oah.wrespHeader 160 } 161 162 // If GetArgs.Writer is specified GetObject will use it to write the response body; 163 // otherwise, it'll `io.Discard` the latter. 164 // 165 // `io.Copy` is used internally to copy response bytes from the request to the writer. 166 // 167 // Returns `ObjAttrs` that can be further used to get the size and other object metadata. 168 func GetObject(bp BaseParams, bck cmn.Bck, objName string, args *GetArgs) (oah ObjAttrs, err error) { 169 var ( 170 wresp *wrappedResp 171 w, q, hdr = args.ret() 172 ) 173 bp.Method = http.MethodGet 174 reqParams := AllocRp() 175 { 176 reqParams.BaseParams = bp 177 reqParams.Path = apc.URLPathObjects.Join(bck.Name, objName) 178 reqParams.Query = bck.NewQuery() 179 reqParams.Header = hdr 180 } 181 // copy qparams over, if any 182 for k, vs := range q { 183 var v string 184 if len(vs) > 0 { 185 v = vs[0] 186 } 187 reqParams.Query.Set(k, v) 188 } 189 wresp, err = reqParams.doWriter(w) 190 FreeRp(reqParams) 191 if err == nil { 192 oah.wrespHeader, oah.n = wresp.Header, wresp.n 193 } 194 return 195 } 196 197 // Same as above with checksum validation. 198 // 199 // Returns `cmn.ErrInvalidCksum` when the expected and actual checksum values 200 // are different. 201 func GetObjectWithValidation(bp BaseParams, bck cmn.Bck, objName string, args *GetArgs) (oah ObjAttrs, err error) { 202 w, q, hdr := args.ret() 203 bp.Method = http.MethodGet 204 reqParams := AllocRp() 205 { 206 reqParams.BaseParams = bp 207 reqParams.Path = apc.URLPathObjects.Join(bck.Name, objName) 208 reqParams.Query = bck.AddToQuery(q) 209 reqParams.Header = hdr 210 } 211 212 var ( 213 resp *http.Response 214 wresp *wrappedResp 215 ) 216 resp, err = reqParams.do() 217 if err != nil { 218 return 219 } 220 221 wresp, err = reqParams.readValidate(resp, w) 222 cos.DrainReader(resp.Body) 223 resp.Body.Close() 224 FreeRp(reqParams) 225 if err == nil { 226 oah.wrespHeader, oah.n = wresp.Header, wresp.n 227 } else if err.Error() == errNilCksum { 228 err = fmt.Errorf("%s is not checksummed, cannot validate", bck.Cname(objName)) 229 } 230 return 231 } 232 233 // GetObjectReader returns reader of the requested object. It does not read body 234 // bytes, nor validates a checksum. Caller is responsible for closing the reader. 235 func GetObjectReader(bp BaseParams, bck cmn.Bck, objName string, args *GetArgs) (r io.ReadCloser, size int64, err error) { 236 _, q, hdr := args.ret() 237 q = bck.AddToQuery(q) 238 bp.Method = http.MethodGet 239 reqParams := AllocRp() 240 { 241 reqParams.BaseParams = bp 242 reqParams.Path = apc.URLPathObjects.Join(bck.Name, objName) 243 reqParams.Query = q 244 reqParams.Header = hdr 245 } 246 r, size, err = reqParams.doReader() 247 FreeRp(reqParams) 248 return 249 } 250 251 ///////////// 252 // PutArgs // 253 ///////////// 254 255 func (args *PutArgs) getBody() (io.ReadCloser, error) { return args.Reader.Open() } 256 257 func (args *PutArgs) put(reqArgs *cmn.HreqArgs) (*http.Request, error) { 258 req, err := reqArgs.Req() 259 if err != nil { 260 return nil, newErrCreateHTTPRequest(err) 261 } 262 // Go http doesn't automatically set this for files, so to handle redirect we do it here. 263 req.GetBody = args.getBody 264 if args.Cksum != nil && args.Cksum.Ty() != cos.ChecksumNone { 265 req.Header.Set(apc.HdrObjCksumType, args.Cksum.Ty()) 266 ckVal := args.Cksum.Value() 267 if ckVal == "" { 268 _, ckhash, err := cos.CopyAndChecksum(io.Discard, args.Reader, nil, args.Cksum.Ty()) 269 if err != nil { 270 return nil, newErrCreateHTTPRequest(err) 271 } 272 ckVal = hex.EncodeToString(ckhash.Sum()) 273 } 274 req.Header.Set(apc.HdrObjCksumVal, ckVal) 275 } 276 if args.Size != 0 { 277 req.ContentLength = int64(args.Size) // as per https://tools.ietf.org/html/rfc7230#section-3.3.2 278 } 279 SetAuxHeaders(req, &args.BaseParams) 280 return req, nil 281 } 282 283 //////////////// 284 // AppendArgs // 285 //////////////// 286 287 func (args *AppendArgs) getBody() (io.ReadCloser, error) { return args.Reader.Open() } 288 289 func (args *AppendArgs) _append(reqArgs *cmn.HreqArgs) (*http.Request, error) { 290 req, err := reqArgs.Req() 291 if err != nil { 292 return nil, newErrCreateHTTPRequest(err) 293 } 294 // The HTTP package doesn't automatically set this for files, so it has to be done manually 295 // If it wasn't set, we would need to deal with the redirect manually. 296 req.GetBody = args.getBody 297 if args.Size != 0 { 298 req.ContentLength = args.Size // as per https://tools.ietf.org/html/rfc7230#section-3.3.2 299 } 300 SetAuxHeaders(req, &args.BaseParams) 301 return req, nil 302 } 303 304 // HeadObject returns object properties; can be conventionally used to establish in-cluster presence. 305 // - fltPresence: as per QparamFltPresence enum (for values and comments, see api/apc/query.go) 306 // - silent==true: not to log (not-found) error 307 func HeadObject(bp BaseParams, bck cmn.Bck, objName string, fltPresence int, silent bool) (*cmn.ObjectProps, error) { 308 bp.Method = http.MethodHead 309 310 q := bck.NewQuery() 311 q.Set(apc.QparamFltPresence, strconv.Itoa(fltPresence)) 312 if silent { 313 q.Set(apc.QparamSilent, "true") 314 } 315 316 reqParams := AllocRp() 317 defer FreeRp(reqParams) 318 { 319 reqParams.BaseParams = bp 320 reqParams.Path = apc.URLPathObjects.Join(bck.Name, objName) 321 reqParams.Query = q 322 } 323 hdr, _, err := reqParams.doReqHdr() 324 if err != nil { 325 return nil, err 326 } 327 if fltPresence == apc.FltPresentNoProps { 328 return nil, err 329 } 330 331 // first, cnm.ObjAttrs (NOTE: compare with `headObject()` in target.go) 332 op := &cmn.ObjectProps{} 333 op.Cksum = op.ObjAttrs.FromHeader(hdr) 334 // second, all the rest 335 err = cmn.IterFields(op, func(tag string, field cmn.IterField) (error, bool) { 336 headerName := apc.PropToHeader(tag) 337 // skip the missing ones 338 if _, ok := hdr[textproto.CanonicalMIMEHeaderKey(headerName)]; !ok { 339 return nil, false 340 } 341 // single-value 342 return field.SetValue(hdr.Get(headerName), true /*force*/), false 343 }, cmn.IterOpts{OnlyRead: false}) 344 if err != nil { 345 return nil, err 346 } 347 return op, nil 348 } 349 350 // Given cos.StrKVs (map[string]string) keys and values, sets object's custom properties. 351 // By default, adds new or updates existing custom keys. 352 // Use `setNewCustomMDFlag` to _replace_ all existing keys with the specified (new) ones. 353 // See also: HeadObject() and apc.HdrObjCustomMD 354 func SetObjectCustomProps(bp BaseParams, bck cmn.Bck, objName string, custom cos.StrKVs, setNew bool) error { 355 var ( 356 actMsg = apc.ActMsg{Value: custom} 357 q url.Values 358 ) 359 if setNew { 360 q = make(url.Values, 4) 361 q = bck.AddToQuery(q) 362 q.Set(apc.QparamNewCustom, "true") 363 } else { 364 q = bck.AddToQuery(q) 365 } 366 bp.Method = http.MethodPatch 367 reqParams := AllocRp() 368 { 369 reqParams.BaseParams = bp 370 reqParams.Path = apc.URLPathObjects.Join(bck.Name, objName) 371 reqParams.Body = cos.MustMarshal(actMsg) 372 reqParams.Header = http.Header{cos.HdrContentType: []string{cos.ContentJSON}} 373 reqParams.Query = q 374 } 375 err := reqParams.DoRequest() 376 FreeRp(reqParams) 377 return err 378 } 379 380 func DeleteObject(bp BaseParams, bck cmn.Bck, objName string) error { 381 bp.Method = http.MethodDelete 382 reqParams := AllocRp() 383 { 384 reqParams.BaseParams = bp 385 reqParams.Path = apc.URLPathObjects.Join(bck.Name, objName) 386 reqParams.Query = bck.NewQuery() 387 } 388 err := reqParams.DoRequest() 389 FreeRp(reqParams) 390 return err 391 } 392 393 func EvictObject(bp BaseParams, bck cmn.Bck, objName string) error { 394 bp.Method = http.MethodDelete 395 actMsg := apc.ActMsg{Action: apc.ActEvictObjects, Name: cos.JoinWords(bck.Name, objName)} 396 reqParams := AllocRp() 397 { 398 reqParams.BaseParams = bp 399 reqParams.Path = apc.URLPathObjects.Join(bck.Name, objName) 400 reqParams.Body = cos.MustMarshal(actMsg) 401 reqParams.Header = http.Header{cos.HdrContentType: []string{cos.ContentJSON}} 402 reqParams.Query = bck.NewQuery() 403 } 404 err := reqParams.DoRequest() 405 FreeRp(reqParams) 406 return err 407 } 408 409 // prefetch object - a convenience method added for "symmetry" with the evict (above) 410 // - compare with api.PrefetchList and api.PrefetchRange 411 func PrefetchObject(bp BaseParams, bck cmn.Bck, objName string) (string, error) { 412 var msg apc.PrefetchMsg 413 msg.ObjNames = []string{objName} 414 return Prefetch(bp, bck, msg) 415 } 416 417 // PutObject PUTs the specified reader (`args.Reader`) as a new object 418 // (or a new version of the object) it in the specified bucket. 419 // 420 // Assumes that `args.Reader` is already opened and ready for usage. 421 // Returns `ObjAttrs` that can be further used to get the size and other object metadata. 422 func PutObject(args *PutArgs) (oah ObjAttrs, err error) { 423 var ( 424 resp *http.Response 425 query = args.Bck.NewQuery() 426 ) 427 if args.SkipVC { 428 query.Set(apc.QparamSkipVC, "true") 429 } 430 reqArgs := cmn.AllocHra() 431 { 432 reqArgs.Method = http.MethodPut 433 reqArgs.Base = args.BaseParams.URL 434 reqArgs.Path = apc.URLPathObjects.Join(args.Bck.Name, args.ObjName) 435 reqArgs.Query = query 436 reqArgs.BodyR = args.Reader 437 } 438 resp, err = DoWithRetry(args.BaseParams.Client, args.put, reqArgs) //nolint:bodyclose // is closed inside 439 cmn.FreeHra(reqArgs) 440 if err == nil { 441 oah.wrespHeader = resp.Header 442 } 443 return 444 } 445 446 // Archive the content of a reader (`args.Reader` - e.g., an open file). 447 // Destination, depending on the options, can be an existing (.tar, .tgz or .tar.gz, .zip, .tar.lz4) 448 // formatted object (aka "shard") or a new one (or, a new version). 449 // --- 450 // For the updated list of supported archival formats -- aka MIME types -- see cmn/cos/archive.go. 451 // -- 452 // See also: 453 // - api.ArchiveMultiObj(msg.AppendIfExists = true) 454 // - api.AppendObject 455 func PutApndArch(args *PutApndArchArgs) (err error) { 456 q := make(url.Values, 4) 457 q = args.Bck.AddToQuery(q) 458 q.Set(apc.QparamArchpath, args.ArchPath) 459 q.Set(apc.QparamArchmime, args.Mime) 460 461 reqArgs := cmn.AllocHra() 462 { 463 reqArgs.Method = http.MethodPut 464 reqArgs.Base = args.BaseParams.URL 465 reqArgs.Path = apc.URLPathObjects.Join(args.Bck.Name, args.ObjName) 466 reqArgs.Query = q 467 reqArgs.BodyR = args.Reader 468 } 469 if args.Flags != 0 { 470 flags := strconv.FormatInt(args.Flags, 10) 471 reqArgs.Header = http.Header{apc.HdrPutApndArchFlags: []string{flags}} 472 } 473 putArgs := &args.PutArgs 474 _, err = DoWithRetry(args.BaseParams.Client, putArgs.put, reqArgs) //nolint:bodyclose // is closed inside 475 cmn.FreeHra(reqArgs) 476 return 477 } 478 479 // AppendObject adds a reader (`args.Reader` - e.g., an open file) to an object. 480 // The API can be called multiple times - each call returns a handle 481 // that may be used for subsequent append requests. 482 // Once all the "appending" is done, the caller must call `api.FlushObject` 483 // to finalize the object. 484 // NOTE: object becomes visible and accessible only _after_ the call to `api.FlushObject`. 485 func AppendObject(args *AppendArgs) (string /*handle*/, error) { 486 q := make(url.Values, 4) 487 q.Set(apc.QparamAppendType, apc.AppendOp) 488 q.Set(apc.QparamAppendHandle, args.Handle) 489 q = args.Bck.AddToQuery(q) 490 491 reqArgs := cmn.AllocHra() 492 { 493 reqArgs.Method = http.MethodPut 494 reqArgs.Base = args.BaseParams.URL 495 reqArgs.Path = apc.URLPathObjects.Join(args.Bck.Name, args.Object) 496 reqArgs.Query = q 497 reqArgs.BodyR = args.Reader 498 } 499 wresp, err := DoWithRetry(args.BaseParams.Client, args._append, reqArgs) //nolint:bodyclose // it's closed inside 500 cmn.FreeHra(reqArgs) 501 if err != nil { 502 return "", err 503 } 504 return wresp.Header.Get(apc.HdrAppendHandle), err 505 } 506 507 // FlushObject must be called after all the appends (via `api.AppendObject`). 508 // To "flush", it uses the handle returned by `api.AppendObject`. 509 // This call will create a fully operational and accessible object. 510 func FlushObject(args *FlushArgs) error { 511 var ( 512 header http.Header 513 q = make(url.Values, 4) 514 method = args.BaseParams.Method 515 ) 516 q.Set(apc.QparamAppendType, apc.FlushOp) 517 q.Set(apc.QparamAppendHandle, args.Handle) 518 q = args.Bck.AddToQuery(q) 519 520 if args.Cksum != nil && args.Cksum.Ty() != cos.ChecksumNone { 521 header = make(http.Header) 522 header.Set(apc.HdrObjCksumType, args.Cksum.Ty()) 523 header.Set(apc.HdrObjCksumVal, args.Cksum.Val()) 524 } 525 args.BaseParams.Method = http.MethodPut 526 reqParams := AllocRp() 527 { 528 reqParams.BaseParams = args.BaseParams 529 reqParams.Path = apc.URLPathObjects.Join(args.Bck.Name, args.Object) 530 reqParams.Query = q 531 reqParams.Header = header 532 } 533 err := reqParams.DoRequest() 534 FreeRp(reqParams) 535 args.BaseParams.Method = method 536 return err 537 } 538 539 // RenameObject renames object name from `oldName` to `newName`. Works only 540 // across single, specified bucket. 541 func RenameObject(bp BaseParams, bck cmn.Bck, oldName, newName string) error { 542 bp.Method = http.MethodPost 543 reqParams := AllocRp() 544 { 545 reqParams.BaseParams = bp 546 reqParams.Path = apc.URLPathObjects.Join(bck.Name, oldName) 547 reqParams.Body = cos.MustMarshal(apc.ActMsg{Action: apc.ActRenameObject, Name: newName}) 548 reqParams.Header = http.Header{cos.HdrContentType: []string{cos.ContentJSON}} 549 reqParams.Query = bck.NewQuery() 550 } 551 err := reqParams.DoRequest() 552 FreeRp(reqParams) 553 return err 554 } 555 556 // promote files and directories to ais objects 557 func Promote(bp BaseParams, bck cmn.Bck, args *apc.PromoteArgs) (xid string, err error) { 558 actMsg := apc.ActMsg{Action: apc.ActPromote, Name: args.SrcFQN, Value: args} 559 bp.Method = http.MethodPost 560 reqParams := AllocRp() 561 { 562 reqParams.BaseParams = bp 563 reqParams.Path = apc.URLPathObjects.Join(bck.Name) 564 reqParams.Body = cos.MustMarshal(actMsg) 565 reqParams.Header = http.Header{cos.HdrContentType: []string{cos.ContentJSON}} 566 reqParams.Query = bck.NewQuery() 567 } 568 _, err = reqParams.doReqStr(&xid) 569 FreeRp(reqParams) 570 return xid, err 571 } 572 573 // DoWithRetry executes `http-client.Do` and retries *retriable connection errors*, 574 // such as "broken pipe" and "connection refused". 575 // This function always closes the `reqArgs.BodR`, even in case of error. 576 // Usage: PUT and simlar requests that transfer payload from the user side. 577 // NOTE: always closes request body reader (reqArgs.BodyR) - explicitly or via Do() 578 // TODO: refactor 579 func DoWithRetry(client *http.Client, cb NewRequestCB, reqArgs *cmn.HreqArgs) (resp *http.Response, err error) { 580 var ( 581 req *http.Request 582 doErr error 583 sleep = httpRetrySleep 584 reader = reqArgs.BodyR.(cos.ReadOpenCloser) 585 ) 586 // first time 587 if req, err = cb(reqArgs); err != nil { 588 cos.Close(reader) 589 return 590 } 591 resp, doErr = client.Do(req) 592 err = doErr 593 if !_retry(doErr, resp) { 594 goto exit 595 } 596 if resp != nil && resp.StatusCode == http.StatusTooManyRequests { 597 sleep = httpRetryRateSleep 598 } 599 600 // retry 601 for range httpMaxRetries { 602 var r io.ReadCloser 603 time.Sleep(sleep) 604 sleep += sleep / 2 605 if r, err = reader.Open(); err != nil { 606 _close(resp, doErr) 607 return 608 } 609 reqArgs.BodyR = r 610 611 if req, err = cb(reqArgs); err != nil { 612 cos.Close(r) 613 _close(resp, doErr) 614 return 615 } 616 _close(resp, doErr) 617 resp, doErr = client.Do(req) 618 err = doErr 619 if !_retry(doErr, resp) { 620 goto exit 621 } 622 } 623 exit: 624 if err == nil { 625 reqParams := AllocRp() 626 err = reqParams.checkResp(resp) 627 cos.DrainReader(resp.Body) 628 FreeRp(reqParams) 629 } 630 _close(resp, doErr) 631 return 632 } 633 634 func _close(resp *http.Response, doErr error) { 635 if resp != nil && doErr == nil { 636 cos.Close(resp.Body) 637 } 638 } 639 640 func _retry(err error, resp *http.Response) bool { 641 if resp != nil && resp.StatusCode == http.StatusTooManyRequests { 642 return true 643 } 644 return err != nil && cos.IsRetriableConnErr(err) 645 }