github.com/NVIDIA/aistore@v1.3.23-0.20240517131212-7df6609be51d/ais/test/etl_test.go (about) 1 // Package integration_test. 2 /* 3 * Copyright (c) 2018-2024, NVIDIA CORPORATION. All rights reserved. 4 */ 5 package integration_test 6 7 import ( 8 "bytes" 9 "fmt" 10 "io" 11 "net/http" 12 "net/url" 13 "os" 14 "os/exec" 15 "path/filepath" 16 "reflect" 17 "regexp" 18 "sort" 19 "strings" 20 "testing" 21 "time" 22 23 "github.com/NVIDIA/aistore/api" 24 "github.com/NVIDIA/aistore/api/apc" 25 "github.com/NVIDIA/aistore/cmn" 26 "github.com/NVIDIA/aistore/cmn/cos" 27 "github.com/NVIDIA/aistore/cmn/debug" 28 "github.com/NVIDIA/aistore/core/meta" 29 "github.com/NVIDIA/aistore/ext/etl" 30 "github.com/NVIDIA/aistore/ext/etl/runtime" 31 "github.com/NVIDIA/aistore/memsys" 32 "github.com/NVIDIA/aistore/tools" 33 "github.com/NVIDIA/aistore/tools/cryptorand" 34 "github.com/NVIDIA/aistore/tools/readers" 35 "github.com/NVIDIA/aistore/tools/tassert" 36 "github.com/NVIDIA/aistore/tools/tetl" 37 "github.com/NVIDIA/aistore/tools/tlog" 38 "github.com/NVIDIA/aistore/tools/trand" 39 "github.com/NVIDIA/aistore/xact" 40 "github.com/NVIDIA/go-tfdata/tfdata/core" 41 ) 42 43 const ( 44 tar2tfIn = "data/small-mnist-3.tar" 45 tar2tfOut = "data/small-mnist-3.record" 46 47 tar2tfFiltersIn = "data/single-png-cls.tar" 48 tar2tfFiltersOut = "data/single-png-cls-transformed.tfrecord" 49 ) 50 51 type ( 52 transformFunc func(r io.Reader) io.Reader 53 filesEqualFunc func(f1, f2 string) (bool, error) 54 55 testObjConfig struct { 56 transformer string 57 comm string 58 inPath string // optional 59 outPath string // optional 60 transform transformFunc // optional 61 filesEqual filesEqualFunc // optional 62 onlyLong bool // run only with long tests 63 } 64 65 testCloudObjConfig struct { 66 cached bool 67 onlyLong bool 68 } 69 ) 70 71 func (tc *testObjConfig) Name() string { 72 return fmt.Sprintf("%s/%s", tc.transformer, strings.TrimSuffix(tc.comm, "://")) 73 } 74 75 // TODO: This should be a part of go-tfdata. 76 // This function is necessary, as the same TFRecords can be different byte-wise. 77 // This is caused by the fact that order of TFExamples is can de different, 78 // as well as ordering of elements of a single TFExample can be different. 79 func tfDataEqual(n1, n2 string) (bool, error) { 80 examples1, err := readExamples(n1) 81 if err != nil { 82 return false, err 83 } 84 examples2, err := readExamples(n2) 85 if err != nil { 86 return false, err 87 } 88 89 if len(examples1) != len(examples2) { 90 return false, nil 91 } 92 return tfRecordsEqual(examples1, examples2) 93 } 94 95 func tfRecordsEqual(examples1, examples2 []*core.TFExample) (bool, error) { 96 sort.SliceStable(examples1, func(i, j int) bool { 97 return examples1[i].GetFeature("__key__").String() < examples1[j].GetFeature("__key__").String() 98 }) 99 sort.SliceStable(examples2, func(i, j int) bool { 100 return examples2[i].GetFeature("__key__").String() < examples2[j].GetFeature("__key__").String() 101 }) 102 103 for i := range len(examples1) { 104 if !reflect.DeepEqual(examples1[i].ProtoReflect(), examples2[i].ProtoReflect()) { 105 return false, nil 106 } 107 } 108 return true, nil 109 } 110 111 func readExamples(fileName string) (examples []*core.TFExample, err error) { 112 f, err := os.Open(fileName) 113 if err != nil { 114 return nil, err 115 } 116 defer f.Close() 117 return core.NewTFRecordReader(f).ReadAllExamples() 118 } 119 120 func testETLObject(t *testing.T, etlName, inPath, outPath string, fTransform transformFunc, fEq filesEqualFunc) { 121 var ( 122 inputFilePath string 123 expectedOutputFilePath string 124 125 proxyURL = tools.RandomProxyURL(t) 126 baseParams = tools.BaseAPIParams(proxyURL) 127 128 bck = cmn.Bck{Provider: apc.AIS, Name: "etl-test"} 129 objName = fmt.Sprintf("%s-%s-object", etlName, trand.String(5)) 130 outputFileName = filepath.Join(t.TempDir(), objName+".out") 131 ) 132 133 buf := make([]byte, 256) 134 _, err := cryptorand.Read(buf) 135 tassert.CheckFatal(t, err) 136 r := bytes.NewReader(buf) 137 138 if inPath != "" { 139 inputFilePath = inPath 140 } else { 141 inputFilePath = tools.CreateFileFromReader(t, "object.in", r) 142 } 143 if outPath != "" { 144 expectedOutputFilePath = outPath 145 } else { 146 _, err := r.Seek(0, io.SeekStart) 147 tassert.CheckFatal(t, err) 148 149 r := fTransform(r) 150 expectedOutputFilePath = tools.CreateFileFromReader(t, "object.out", r) 151 } 152 153 tools.CreateBucket(t, proxyURL, bck, nil, true /*cleanup*/) 154 155 tlog.Logln("PUT object") 156 reader, err := readers.NewExistingFile(inputFilePath, cos.ChecksumNone) 157 tassert.CheckFatal(t, err) 158 tools.PutObject(t, bck, objName, reader) 159 160 fho, err := cos.CreateFile(outputFileName) 161 tassert.CheckFatal(t, err) 162 defer fho.Close() 163 164 tlog.Logf("GET %s via etl[%s]\n", bck.Cname(objName), etlName) 165 err = api.ETLObject(baseParams, etlName, bck, objName, fho) 166 tassert.CheckFatal(t, err) 167 168 tlog.Logln("Compare output") 169 same, err := fEq(outputFileName, expectedOutputFilePath) 170 tassert.CheckError(t, err) 171 tassert.Errorf(t, same, "file contents after transformation differ") 172 } 173 174 func testETLObjectCloud(t *testing.T, bck cmn.Bck, etlName string, onlyLong, cached bool) { 175 var ( 176 proxyURL = tools.RandomProxyURL(t) 177 baseParams = tools.BaseAPIParams(proxyURL) 178 ) 179 180 tools.CheckSkip(t, &tools.SkipTestArgs{Long: onlyLong}) 181 182 // TODO: PUT and then transform many objects 183 184 objName := fmt.Sprintf("%s-%s-object", etlName, trand.String(5)) 185 tlog.Logln("PUT object") 186 reader, err := readers.NewRand(cos.KiB, cos.ChecksumNone) 187 tassert.CheckFatal(t, err) 188 189 _, err = api.PutObject(&api.PutArgs{ 190 BaseParams: baseParams, 191 Bck: bck, 192 ObjName: objName, 193 Reader: reader, 194 }) 195 tassert.CheckFatal(t, err) 196 197 if !cached { 198 tlog.Logf("Evicting object %s\n", bck.Cname(objName)) 199 err := api.EvictObject(baseParams, bck, objName) 200 tassert.CheckFatal(t, err) 201 } 202 203 defer func() { 204 // Could bucket is not destroyed, remove created object instead. 205 err := api.DeleteObject(baseParams, bck, objName) 206 tassert.CheckError(t, err) 207 }() 208 209 bf := bytes.NewBuffer(nil) 210 tlog.Logf("Use ETL[%s] to read transformed object\n", etlName) 211 err = api.ETLObject(baseParams, etlName, bck, objName, bf) 212 tassert.CheckFatal(t, err) 213 tassert.Errorf(t, bf.Len() == cos.KiB, "Expected %d bytes, got %d", cos.KiB, bf.Len()) 214 } 215 216 // NOTE: BytesCount references number of bytes *before* the transformation. 217 func checkETLStats(t *testing.T, xid string, expectedObjCnt int, expectedBytesCnt uint64, skipByteStats bool) { 218 snaps, err := api.QueryXactionSnaps(baseParams, &xact.ArgsMsg{ID: xid}) 219 tassert.CheckFatal(t, err) 220 221 objs, outObjs, inObjs := snaps.ObjCounts(xid) 222 223 tassert.Errorf(t, objs == int64(expectedObjCnt), "expected %d objects, got %d (where sent %d, received %d)", 224 expectedObjCnt, objs, outObjs, inObjs) 225 if outObjs != inObjs { 226 tlog.Logf("Warning: (sent objects) %d != %d (received objects)\n", outObjs, inObjs) 227 } else { 228 tlog.Logf("Num sent/received objects: %d\n", outObjs) 229 } 230 231 if skipByteStats { 232 return // don't know the size 233 } 234 bytes, outBytes, inBytes := snaps.ByteCounts(xid) 235 236 // TODO -- FIXME: validate transformed bytes as well, make sure `expectedBytesCnt` is correct 237 238 tlog.Logf("Byte counts: expected %d, got (original %d, sent %d, received %d)\n", expectedBytesCnt, 239 bytes, outBytes, inBytes) 240 } 241 242 func TestETLObject(t *testing.T) { 243 tools.CheckSkip(t, &tools.SkipTestArgs{RequiredDeployment: tools.ClusterTypeK8s}) 244 tetl.CheckNoRunningETLContainers(t, baseParams) 245 246 noopTransform := func(r io.Reader) io.Reader { return r } 247 tests := []testObjConfig{ 248 {transformer: tetl.Echo, comm: etl.Hpull, transform: noopTransform, filesEqual: tools.FilesEqual, onlyLong: true}, 249 {transformer: tetl.Echo, comm: etl.Hrev, transform: noopTransform, filesEqual: tools.FilesEqual, onlyLong: true}, 250 {transformer: tetl.Echo, comm: etl.Hpush, transform: noopTransform, filesEqual: tools.FilesEqual, onlyLong: true}, 251 {tetl.Tar2TF, etl.Hpull, tar2tfIn, tar2tfOut, nil, tfDataEqual, true}, 252 {tetl.Tar2TF, etl.Hrev, tar2tfIn, tar2tfOut, nil, tfDataEqual, true}, 253 {tetl.Tar2TF, etl.Hpush, tar2tfIn, tar2tfOut, nil, tfDataEqual, true}, 254 {tetl.Tar2tfFilters, etl.Hpull, tar2tfFiltersIn, tar2tfFiltersOut, nil, tfDataEqual, false}, 255 {tetl.Tar2tfFilters, etl.Hrev, tar2tfFiltersIn, tar2tfFiltersOut, nil, tfDataEqual, false}, 256 {tetl.Tar2tfFilters, etl.Hpush, tar2tfFiltersIn, tar2tfFiltersOut, nil, tfDataEqual, false}, 257 } 258 259 for _, test := range tests { 260 t.Run(test.Name(), func(t *testing.T) { 261 tools.CheckSkip(t, &tools.SkipTestArgs{Long: test.onlyLong}) 262 263 _ = tetl.InitSpec(t, baseParams, test.transformer, test.comm) 264 t.Cleanup(func() { tetl.StopAndDeleteETL(t, baseParams, test.transformer) }) 265 266 testETLObject(t, test.transformer, test.inPath, test.outPath, test.transform, test.filesEqual) 267 }) 268 } 269 } 270 271 func TestETLObjectCloud(t *testing.T) { 272 tools.CheckSkip(t, &tools.SkipTestArgs{Bck: cliBck, RequiredDeployment: tools.ClusterTypeK8s, RemoteBck: true}) 273 tetl.CheckNoRunningETLContainers(t, baseParams) 274 275 tcs := map[string][]*testCloudObjConfig{ 276 etl.Hpull: { 277 {cached: true, onlyLong: false}, 278 {cached: false, onlyLong: false}, 279 }, 280 etl.Hrev: { 281 {cached: true, onlyLong: false}, 282 {cached: false, onlyLong: false}, 283 }, 284 etl.Hpush: { 285 {cached: true, onlyLong: false}, 286 {cached: false, onlyLong: false}, 287 }, 288 } 289 290 for comm, configs := range tcs { 291 t.Run(comm, func(t *testing.T) { 292 // TODO: currently, Echo transformation only - add other transforms 293 _ = tetl.InitSpec(t, baseParams, tetl.Echo, comm) 294 t.Cleanup(func() { tetl.StopAndDeleteETL(t, baseParams, tetl.Echo) }) 295 296 for _, conf := range configs { 297 t.Run(fmt.Sprintf("cached=%t", conf.cached), func(t *testing.T) { 298 testETLObjectCloud(t, cliBck, tetl.Echo, conf.onlyLong, conf.cached) 299 }) 300 } 301 }) 302 } 303 } 304 305 // TODO: initial impl - revise and add many more tests 306 func TestETLInline(t *testing.T) { 307 tools.CheckSkip(t, &tools.SkipTestArgs{RequiredDeployment: tools.ClusterTypeK8s}) 308 tetl.CheckNoRunningETLContainers(t, baseParams) 309 310 var ( 311 proxyURL = tools.RandomProxyURL(t) 312 baseParams = tools.BaseAPIParams(proxyURL) 313 314 bck = cmn.Bck{Provider: apc.AIS, Name: "etl-test"} 315 316 tests = []testObjConfig{ 317 {transformer: tetl.MD5, comm: etl.Hpush}, 318 } 319 ) 320 321 for _, test := range tests { 322 t.Run(test.Name(), func(t *testing.T) { 323 tools.CheckSkip(t, &tools.SkipTestArgs{Long: test.onlyLong}) 324 325 _ = tetl.InitSpec(t, baseParams, test.transformer, test.comm) 326 t.Cleanup(func() { tetl.StopAndDeleteETL(t, baseParams, test.transformer) }) 327 328 tools.CreateBucket(t, proxyURL, bck, nil, true /*cleanup*/) 329 330 tlog.Logln("PUT object") 331 objNames, _, err := tools.PutRandObjs(tools.PutObjectsArgs{ 332 ProxyURL: proxyURL, 333 Bck: bck, 334 ObjCnt: 1, 335 ObjSize: cos.MiB, 336 }) 337 tassert.CheckFatal(t, err) 338 objName := objNames[0] 339 340 tlog.Logln("GET transformed object") 341 outObject := bytes.NewBuffer(nil) 342 _, err = api.GetObject(baseParams, bck, objName, &api.GetArgs{ 343 Writer: outObject, 344 Query: url.Values{apc.QparamETLName: {test.transformer}}, 345 }) 346 tassert.CheckFatal(t, err) 347 348 matchesMD5 := regexp.MustCompile("^[a-fA-F0-9]{32}$").MatchReader(outObject) 349 tassert.Fatalf(t, matchesMD5, "expected transformed object to be md5 checksum") 350 }) 351 } 352 } 353 354 func TestETLInlineMD5SingleObj(t *testing.T) { 355 var ( 356 proxyURL = tools.RandomProxyURL(t) 357 baseParams = tools.BaseAPIParams(proxyURL) 358 359 bck = cmn.Bck{Provider: apc.AIS, Name: "etl-test"} 360 transformer = tetl.MD5 361 comm = etl.Hpush 362 ) 363 tools.CheckSkip(t, &tools.SkipTestArgs{RequiredDeployment: tools.ClusterTypeK8s}) 364 tetl.CheckNoRunningETLContainers(t, baseParams) 365 366 _ = tetl.InitSpec(t, baseParams, transformer, comm) 367 t.Cleanup(func() { tetl.StopAndDeleteETL(t, baseParams, transformer) }) 368 369 tools.CreateBucket(t, proxyURL, bck, nil, true /*cleanup*/) 370 371 tlog.Logln("PUT object") 372 objName := trand.String(10) 373 reader, err := readers.NewRand(cos.MiB, cos.ChecksumMD5) 374 tassert.CheckFatal(t, err) 375 376 _, err = api.PutObject(&api.PutArgs{ 377 BaseParams: baseParams, 378 Bck: bck, 379 ObjName: objName, 380 Reader: reader, 381 }) 382 tassert.CheckFatal(t, err) 383 384 tlog.Logln("GET transformed object") 385 outObject := memsys.PageMM().NewSGL(0) 386 defer outObject.Free() 387 388 _, err = api.GetObject(baseParams, bck, objName, &api.GetArgs{ 389 Writer: outObject, 390 Query: url.Values{apc.QparamETLName: {transformer}}, 391 }) 392 tassert.CheckFatal(t, err) 393 394 exp, got := reader.Cksum().Val(), string(outObject.Bytes()) 395 tassert.Errorf(t, exp == got, "expected transformed object to be md5 checksum %s, got %s", exp, 396 got[:min(len(got), 16)]) 397 } 398 399 func TestETLAnyToAnyBucket(t *testing.T) { 400 tools.CheckSkip(t, &tools.SkipTestArgs{RequiredDeployment: tools.ClusterTypeK8s}) 401 tetl.CheckNoRunningETLContainers(t, baseParams) 402 403 var ( 404 proxyURL = tools.RandomProxyURL(t) 405 baseParams = tools.BaseAPIParams(proxyURL) 406 objCnt = 100 407 408 bcktests = []struct { 409 srcRemote bool 410 evictRemoteSrc bool 411 dstRemote bool 412 }{ 413 {false, false, false}, 414 {true, false, false}, 415 {true, true, false}, 416 {false, false, true}, 417 } 418 tests = []testObjConfig{ 419 {transformer: tetl.Echo, comm: etl.Hpull, onlyLong: true}, 420 {transformer: tetl.MD5, comm: etl.Hrev}, 421 {transformer: tetl.MD5, comm: etl.Hpush, onlyLong: true}, 422 } 423 ) 424 425 for _, bcktest := range bcktests { 426 m := ioContext{ 427 t: t, 428 num: objCnt, 429 fileSize: 512, 430 fixedSize: true, // see checkETLStats below 431 } 432 if bcktest.srcRemote { 433 m.bck = cliBck 434 m.deleteRemoteBckObjs = true 435 } else { 436 m.bck = cmn.Bck{Name: "etlsrc_" + cos.GenTie(), Provider: apc.AIS} 437 tools.CreateBucket(t, proxyURL, m.bck, nil, true /*cleanup*/) 438 } 439 m.init(true /*cleanup*/) 440 441 if bcktest.srcRemote { 442 m.remotePuts(false) // (deleteRemoteBckObjs above) 443 if bcktest.evictRemoteSrc { 444 tlog.Logf("evicting %s\n", m.bck) 445 // 446 // evict all _cached_ data from the "local" cluster 447 // keep the src bucket in the "local" BMD though 448 // 449 err := api.EvictRemoteBucket(baseParams, m.bck, true /*keep empty src bucket in the BMD*/) 450 tassert.CheckFatal(t, err) 451 } 452 } else { 453 m.puts() 454 } 455 456 for _, test := range tests { 457 // NOTE: have to use one of the predefined etlName which, by coincidence, 458 // corresponds to the test.transformer name and is further used to resolve 459 // the corresponding init-spec yaml, e.g.: 460 // https://raw.githubusercontent.com/NVIDIA/ais-etl/master/transformers/md5/pod.yaml" 461 // See also: tetl.validateETLName 462 etlName := test.transformer 463 464 tname := fmt.Sprintf("%s-%s", test.transformer, strings.TrimSuffix(test.comm, "://")) 465 if bcktest.srcRemote { 466 if bcktest.evictRemoteSrc { 467 tname += "/from-evicted-remote" 468 } else { 469 tname += "/from-remote" 470 } 471 } else { 472 debug.Assert(!bcktest.evictRemoteSrc) 473 tname += "/from-ais" 474 } 475 if bcktest.dstRemote { 476 tname += "/to-remote" 477 } else { 478 tname += "/to-ais" 479 } 480 t.Run(tname, func(t *testing.T) { 481 tools.CheckSkip(t, &tools.SkipTestArgs{Long: test.onlyLong}) 482 _ = tetl.InitSpec(t, baseParams, etlName, test.comm) 483 484 var bckTo cmn.Bck 485 if bcktest.dstRemote { 486 bckTo = cliBck 487 dstm := ioContext{t: t, bck: bckTo} 488 dstm.del() 489 t.Cleanup(func() { dstm.del() }) 490 } else { 491 bckTo = cmn.Bck{Name: "etldst_" + cos.GenTie(), Provider: apc.AIS} 492 // NOTE: ais will create dst bucket on the fly 493 494 t.Cleanup(func() { tools.DestroyBucket(t, proxyURL, bckTo) }) 495 } 496 testETLBucket(t, baseParams, etlName, &m, bckTo, time.Minute, false, bcktest.evictRemoteSrc) 497 }) 498 } 499 } 500 } 501 502 // also responsible for cleanup: ETL xaction, ETL containers, destination bucket. 503 func testETLBucket(t *testing.T, bp api.BaseParams, etlName string, m *ioContext, bckTo cmn.Bck, timeout time.Duration, 504 skipByteStats, evictRemoteSrc bool) { 505 var ( 506 xid, kind string 507 err error 508 bckFrom = m.bck 509 requestTimeout = 30 * time.Second 510 511 msg = &apc.TCBMsg{ 512 Transform: apc.Transform{ 513 Name: etlName, 514 Timeout: cos.Duration(requestTimeout), 515 }, 516 CopyBckMsg: apc.CopyBckMsg{Force: true}, 517 } 518 ) 519 520 t.Cleanup(func() { tetl.StopAndDeleteETL(t, bp, etlName) }) 521 tlog.Logf("Start ETL[%s]: %s => %s ...\n", etlName, bckFrom.Cname(""), bckTo.Cname("")) 522 523 if evictRemoteSrc { 524 kind = apc.ActETLObjects // TODO -- FIXME: remove/simplify-out the reliance on x-kind 525 xid, err = api.ETLBucket(bp, bckFrom, bckTo, msg, apc.FltExists) 526 } else { 527 kind = apc.ActETLBck 528 xid, err = api.ETLBucket(bp, bckFrom, bckTo, msg) 529 } 530 tassert.CheckFatal(t, err) 531 532 t.Cleanup(func() { 533 if bckTo.IsRemote() { 534 err = api.EvictRemoteBucket(bp, bckTo, false /*keep md*/) 535 tassert.CheckFatal(t, err) 536 tlog.Logf("[cleanup] %s evicted\n", bckTo) 537 } else { 538 tools.DestroyBucket(t, bp.URL, bckTo) 539 } 540 }) 541 542 tlog.Logf("ETL[%s]: running %s => %s x-etl[%s]\n", etlName, bckFrom.Cname(""), bckTo.Cname(""), xid) 543 544 err = tetl.WaitForFinished(bp, xid, kind, timeout) 545 tassert.CheckFatal(t, err) 546 547 list, err := api.ListObjects(bp, bckTo, nil, api.ListArgs{}) 548 tassert.CheckFatal(t, err) 549 tassert.Errorf(t, len(list.Entries) == m.num, "expected %d objects, got %d", m.num, len(list.Entries)) 550 551 checkETLStats(t, xid, m.num, m.fileSize*uint64(m.num), skipByteStats) 552 } 553 554 func TestETLInitCode(t *testing.T) { 555 tools.CheckSkip(t, &tools.SkipTestArgs{RequiredDeployment: tools.ClusterTypeK8s}) 556 tetl.CheckNoRunningETLContainers(t, baseParams) 557 558 const ( 559 md5 = ` 560 import hashlib 561 562 def transform(input_bytes): 563 md5 = hashlib.md5() 564 md5.update(input_bytes) 565 return md5.hexdigest().encode() 566 ` 567 echo = ` 568 def transform(reader, w): 569 for chunk in reader: 570 w.write(chunk) 571 ` 572 573 md5IO = ` 574 import hashlib 575 import sys 576 577 md5 = hashlib.md5() 578 chunk = sys.stdin.buffer.read() 579 md5.update(chunk) 580 sys.stdout.buffer.write(md5.hexdigest().encode()) 581 ` 582 583 numpy = ` 584 import numpy as np 585 586 def transform(input_bytes: bytes) -> bytes: 587 x = np.array([[0, 1], [2, 3]], dtype='<u2') 588 return x.tobytes() 589 ` 590 numpyDeps = `numpy==1.19.2` 591 ) 592 593 var ( 594 proxyURL = tools.RandomProxyURL(t) 595 baseParams = tools.BaseAPIParams(proxyURL) 596 597 m = ioContext{ 598 t: t, 599 num: 10, 600 fileSize: 512, 601 fixedSize: true, 602 bck: cmn.Bck{Name: "etl_build", Provider: apc.AIS}, 603 } 604 605 tests = []struct { 606 etlName string 607 code string 608 deps string 609 runtime string 610 commType string 611 chunkSize int64 612 onlyLong bool 613 }{ 614 {etlName: "simple-py38", code: md5, deps: "", runtime: runtime.Py38, onlyLong: false}, 615 {etlName: "simple-py38-stream", code: echo, deps: "", runtime: runtime.Py38, onlyLong: false, chunkSize: 64}, 616 {etlName: "with-deps-py38", code: numpy, deps: numpyDeps, runtime: runtime.Py38, onlyLong: false}, 617 {etlName: "simple-py310-io", code: md5IO, deps: "", runtime: runtime.Py310, commType: etl.HpushStdin, onlyLong: false}, 618 } 619 ) 620 621 tools.CreateBucket(t, proxyURL, m.bck, nil, true /*cleanup*/) 622 623 m.init(true /*cleanup*/) 624 625 m.puts() 626 627 for _, testType := range []string{"etl_object", "etl_bucket"} { 628 for _, test := range tests { 629 t.Run(testType+"__"+test.etlName, func(t *testing.T) { 630 tools.CheckSkip(t, &tools.SkipTestArgs{Long: test.onlyLong}) 631 632 msg := etl.InitCodeMsg{ 633 InitMsgBase: etl.InitMsgBase{ 634 IDX: test.etlName, 635 CommTypeX: test.commType, 636 Timeout: etlBucketTimeout, 637 }, 638 Code: []byte(test.code), 639 Deps: []byte(test.deps), 640 Runtime: test.runtime, 641 ChunkSize: test.chunkSize, 642 } 643 msg.Funcs.Transform = "transform" 644 645 _ = tetl.InitCode(t, baseParams, &msg) 646 647 switch testType { 648 case "etl_object": 649 t.Cleanup(func() { tetl.StopAndDeleteETL(t, baseParams, test.etlName) }) 650 651 testETLObject(t, test.etlName, "", "", func(r io.Reader) io.Reader { 652 return r // TODO: Write function to transform input to md5. 653 }, func(_, _ string) (bool, error) { 654 return true, nil // TODO: Write function to compare output from md5. 655 }) 656 case "etl_bucket": 657 bckTo := cmn.Bck{Name: "etldst_" + cos.GenTie(), Provider: apc.AIS} 658 testETLBucket(t, baseParams, test.etlName, &m, bckTo, time.Minute, 659 false /*skip checking byte counts*/, false /* remote src evicted */) 660 default: 661 panic(testType) 662 } 663 }) 664 } 665 } 666 } 667 668 func TestETLBucketDryRun(t *testing.T) { 669 tools.CheckSkip(t, &tools.SkipTestArgs{RequiredDeployment: tools.ClusterTypeK8s}) 670 tetl.CheckNoRunningETLContainers(t, baseParams) 671 672 var ( 673 proxyURL = tools.RandomProxyURL(t) 674 baseParams = tools.BaseAPIParams(proxyURL) 675 676 bckFrom = cmn.Bck{Name: "etloffline", Provider: apc.AIS} 677 bckTo = cmn.Bck{Name: "etloffline-out-" + trand.String(5), Provider: apc.AIS} 678 objCnt = 10 679 680 m = ioContext{ 681 t: t, 682 num: objCnt, 683 fileSize: 512, 684 fixedSize: true, 685 bck: bckFrom, 686 } 687 ) 688 689 tools.CreateBucket(t, proxyURL, bckFrom, nil, true /*cleanup*/) 690 m.init(true /*cleanup*/) 691 692 m.puts() 693 694 _ = tetl.InitSpec(t, baseParams, tetl.Echo, etl.Hrev) 695 t.Cleanup(func() { tetl.StopAndDeleteETL(t, baseParams, tetl.Echo) }) 696 697 msg := &apc.TCBMsg{ 698 Transform: apc.Transform{ 699 Name: tetl.Echo, 700 }, 701 CopyBckMsg: apc.CopyBckMsg{DryRun: true, Force: true}, 702 } 703 xid, err := api.ETLBucket(baseParams, bckFrom, bckTo, msg) 704 tassert.CheckFatal(t, err) 705 706 args := xact.ArgsMsg{ID: xid, Timeout: time.Minute} 707 _, err = api.WaitForXactionIC(baseParams, &args) 708 tassert.CheckFatal(t, err) 709 710 exists, err := api.QueryBuckets(baseParams, cmn.QueryBcks(bckTo), apc.FltPresent) 711 tassert.CheckFatal(t, err) 712 tassert.Errorf(t, exists == false, "[dry-run] expected destination bucket not to be created") 713 714 checkETLStats(t, xid, m.num, uint64(m.num*int(m.fileSize)), false) 715 } 716 717 func TestETLStopAndRestartETL(t *testing.T) { 718 tools.CheckSkip(t, &tools.SkipTestArgs{RequiredDeployment: tools.ClusterTypeK8s}) 719 tetl.CheckNoRunningETLContainers(t, baseParams) 720 721 var ( 722 proxyURL = tools.RandomProxyURL(t) 723 baseParams = tools.BaseAPIParams(proxyURL) 724 etlName = tetl.Echo // TODO: currently, echo only - add more 725 ) 726 727 _ = tetl.InitSpec(t, baseParams, etlName, etl.Hrev) 728 t.Cleanup(func() { tetl.StopAndDeleteETL(t, baseParams, etlName) }) 729 730 // 1. Check ETL is in running state 731 tetl.ETLShouldBeRunning(t, baseParams, etlName) 732 733 // 2. Stop ETL and verify it stopped successfully 734 tlog.Logf("stopping ETL[%s]\n", etlName) 735 err := api.ETLStop(baseParams, etlName) 736 tassert.CheckFatal(t, err) 737 tetl.ETLShouldNotBeRunning(t, baseParams, etlName) 738 739 // 3. Start ETL and verify it is in running state 740 tlog.Logf("restarting ETL[%s]\n", etlName) 741 err = api.ETLStart(baseParams, etlName) 742 tassert.CheckFatal(t, err) 743 tetl.ETLShouldBeRunning(t, baseParams, etlName) 744 } 745 746 func TestETLMultipleTransformersAtATime(t *testing.T) { 747 tools.CheckSkip(t, &tools.SkipTestArgs{RequiredDeployment: tools.ClusterTypeK8s, Long: true}) 748 tetl.CheckNoRunningETLContainers(t, baseParams) 749 750 output, err := exec.Command("bash", "-c", "kubectl get nodes | grep Ready | wc -l").CombinedOutput() 751 tassert.CheckFatal(t, err) 752 if strings.Trim(string(output), "\n") != "1" { 753 t.Skip("Requires a single node kubernetes cluster") 754 } 755 756 if tools.GetClusterMap(t, proxyURL).CountTargets() > 1 { 757 t.Skip("Requires a single-node single-target deployment") 758 } 759 760 _ = tetl.InitSpec(t, baseParams, tetl.Echo, etl.Hrev) 761 t.Cleanup(func() { tetl.StopAndDeleteETL(t, baseParams, tetl.Echo) }) 762 763 _ = tetl.InitSpec(t, baseParams, tetl.MD5, etl.Hrev) 764 t.Cleanup(func() { tetl.StopAndDeleteETL(t, baseParams, tetl.MD5) }) 765 } 766 767 const getMetricsTimeout = 90 * time.Second 768 769 func TestETLHealth(t *testing.T) { 770 var ( 771 proxyURL = tools.RandomProxyURL(t) 772 baseParams = tools.BaseAPIParams(proxyURL) 773 etlName = tetl.Echo // TODO: currently, only echo - add more 774 ) 775 776 tools.CheckSkip(t, &tools.SkipTestArgs{RequiredDeployment: tools.ClusterTypeK8s, Long: true}) 777 tetl.CheckNoRunningETLContainers(t, baseParams) 778 779 _ = tetl.InitSpec(t, baseParams, etlName, etl.Hpull) 780 t.Cleanup(func() { tetl.StopAndDeleteETL(t, baseParams, etlName) }) 781 782 var ( 783 start = time.Now() 784 deadline = start.Add(getMetricsTimeout) // might take a while for metrics to become available 785 healths etl.HealthByTarget 786 err error 787 ) 788 for { 789 now := time.Now() 790 if now.After(deadline) { 791 t.Fatal("Timeout waiting for successful health response") 792 } 793 794 healths, err = api.ETLHealth(baseParams, etlName) 795 if err == nil { 796 if len(healths) > 0 { 797 tlog.Logf("Successfully received health data after %s\n", now.Sub(start)) 798 break 799 } 800 tlog.Logln("Unexpected empty health messages without error, retrying...") 801 continue 802 } 803 804 herr, ok := err.(*cmn.ErrHTTP) 805 tassert.Errorf(t, ok && herr.Status == http.StatusNotFound, "Unexpected error %v, expected 404", err) 806 tlog.Logf("ETL[%s] not found in metrics, retrying...\n", etlName) 807 time.Sleep(10 * time.Second) 808 } 809 810 // TODO -- FIXME: see health handlers returning "OK" - revisit 811 for _, msg := range healths { 812 tassert.Errorf(t, msg.Status == "Running", "Expected pod at %s to be running, got %q", 813 meta.Tname(msg.TargetID), msg.Status) 814 } 815 } 816 817 func TestETLMetrics(t *testing.T) { 818 var ( 819 proxyURL = tools.RandomProxyURL(t) 820 baseParams = tools.BaseAPIParams(proxyURL) 821 etlName = tetl.Echo // TODO: currently, only echo - add more 822 ) 823 824 tools.CheckSkip(t, &tools.SkipTestArgs{RequiredDeployment: tools.ClusterTypeK8s, Long: true}) 825 tetl.CheckNoRunningETLContainers(t, baseParams) 826 827 _ = tetl.InitSpec(t, baseParams, etlName, etl.Hpull) 828 t.Cleanup(func() { tetl.StopAndDeleteETL(t, baseParams, etlName) }) 829 830 var ( 831 start = time.Now() 832 deadline = start.Add(getMetricsTimeout) // might take a while for metrics to become available 833 metrics etl.CPUMemByTarget 834 err error 835 ) 836 for { 837 now := time.Now() 838 if now.After(deadline) { 839 t.Fatal("Timeout waiting for successful metrics response") 840 } 841 842 metrics, err = api.ETLMetrics(baseParams, etlName) 843 if err == nil { 844 if len(metrics) > 0 { 845 tlog.Logf("Successfully received metrics after %s\n", now.Sub(start)) 846 break 847 } 848 tlog.Logln("Unexpected empty metrics messages without error, retrying...") 849 continue 850 } 851 852 herr, ok := err.(*cmn.ErrHTTP) 853 tassert.Errorf(t, ok && herr.Status == http.StatusNotFound, "Unexpected error %v, expected 404", err) 854 tlog.Logf("ETL[%s] not found in metrics, retrying...\n", etlName) 855 time.Sleep(10 * time.Second) 856 } 857 858 for _, metric := range metrics { 859 tassert.Errorf(t, metric.CPU > 0.0 || metric.Mem > 0, "[%s] expected non empty metrics info, got %v", 860 metric.TargetID, metric) 861 } 862 } 863 864 func TestETLList(t *testing.T) { 865 var ( 866 proxyURL = tools.RandomProxyURL(t) 867 baseParams = tools.BaseAPIParams(proxyURL) 868 etlName = tetl.Echo // TODO: currently, only echo - add more 869 ) 870 tools.CheckSkip(t, &tools.SkipTestArgs{RequiredDeployment: tools.ClusterTypeK8s}) 871 872 _ = tetl.InitSpec(t, baseParams, etlName, etl.Hrev) 873 t.Cleanup(func() { tetl.StopAndDeleteETL(t, baseParams, etlName) }) 874 875 list, err := api.ETLList(baseParams) 876 tassert.CheckFatal(t, err) 877 tassert.Fatalf(t, len(list) == 1, "expected exactly one ETL to be listed, got %d (%+v)", len(list), list) 878 tassert.Fatalf(t, list[0].Name == etlName, "expected ETL[%s], got %q", etlName, list[0].Name) 879 }